1. Introduction
According to Kotlin Coroutine library authors, it should be easy to create another coroutine. Since a coroutine is much more lightweight than a thread, we can create one every time we need it.
While we might have read in the documentation that “coroutines are just light threads”, it’s not strictly true. A thread represents system resources allocated for its execution, as well as a sequence of instructions itself. On the other hand, a coroutine contains only a weak link to those resources (via its dispatcher), but it has rather a strong connection to a lifecycle of a caller.
If the calling entity no longer exists, it can’t benefit from the results of the coroutine. Therefore, it’s best to cancel the coroutine as soon as there is no need for its results. This is the structured concurrency paradigm at work: All control flow constructions must have clear entry and exit points, and all threads must finish before the exit.
These considerations require tools to separate coroutine lifecycles for better management. Those tools are launch {} and async {}. Let’s look at these two scope functions more closely.
2. Launching a Coroutine as a Job
Launching a coroutine as a Job is probably the most basic coroutine action. To do it, we have to create a CoroutineScope and then call launch {}:
val job: Job = launch {
println("I am executed, but you only see the side-effects")
}
The launch {} function returns a Job object, on which we can block until all the instructions within the coroutine are complete, or else they throw an exception. The actual execution of a coroutine can also be postponed until we need it with a start argument. If we use CoroutineStart.LAZY, the coroutine will be executed only when someone calls join() on its Job object. The default value is CoroutineStart.DEFAULT and starts the execution right away.
There’s also CoroutineStart.ATOMIC, which prevents coroutine cancellation until it reaches its first suspension point, and CoroutineStart.UNDISPATCHED, which starts execution in the current thread, then suspends at the first suspension point and, on continuation, uses the dispatcher from its context.
In case we have to deal with several scopes with various lifecycles, we might use a more direct form of calling:
val customRoutine = launch {
println("${Thread.currentThread().name}: I am launched in a class scope")
}
val globalRoutine = GlobalScope.launch {
println("${Thread.currentThread().name}: I am launched in a global scope")
}
This form of launching a coroutine immediately returns the control back to the caller. It is usually known as “fire-and-forget”. Certainly, we don’t have to “forget” we invoked the coroutine, but also, the only result we can get is a simple “success” or “failure”.
In case we need a more structured response, we have to use async {}.
3. Launching a Coroutine to Get a Result
An async {} call is similar to launch {} but will return a Deferred
As previously discussed, async calls lend themselves easily to achieve concurrency within the same coroutine scope. We can also call await() within runBlocking {} if that final result is all that we want:
val futureResult: Deferred<String> = async {
"Hello, world!"
}
runBlocking {
println(futureResult.await())
}
As with launch {}, we can be lazy about actually calculating the value of Deferred or affect its thread and cancellability by a start argument:
val first = async(start = CoroutineStart.LAZY) {
println("${Thread.currentThread().name}: I am lazy and launched only now")
"Hello, "
}
val second = async {
println("${Thread.currentThread().name}: I am eager!")
"world!"
}
runBlocking {
println(first.await() + second.await())
}
This fragment will print the output of the second coroutine before the output of the first.
4. Conclusion
In this tutorial, we looked at the similarities and differences of launch {} and async {} scope functions. Both of them allow to run code in a coroutine, but only async {} will allow returning a typed result. launch {} can only produce side-effects.
The code from the samples can be found over on GitHub.