Interactive Stories -Coroutine Builders
Hi,
In the previous guide, we talked about coroutine basics, if you haven’t read it yet, you can consider it from the link. Anyway, coroutines are a big concept like a puzzle. Today, we will dive deeply into coroutine builder, a piece of a puzzle you must understand in a coroutine.
What is a coroutine builder?
Coroutine builders are functions that create a new coroutine to run a given suspending function. We have different types of coroutine builders that launch
, async
, runBlocking
, withContext
, etc.
Fire-and-forget with “launch”
The launch
builder is the simplest way to start a new coroutine that performs a task asynchronously. It is ideal for fire-and-forget scenarios when executing a coroutine without waiting for its result.
import kotlinx.coroutines.*
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}
suspend fun doWorld() {
delay(1000L)
println("World!")
}
When you compile the code block you’ll see the output “Hello World!”. You created a new coroutine for the suspend function doWorld()
operation in the launch
builder.
Anatomy of the “launch”
public fun kotlinx.coroutines.CoroutineScope.launch(
context: kotlin.coroutines.CoroutineContext = COMPILED_CODE,
start: kotlinx.coroutines.CoroutineStart = COMPILED_CODE,
block: suspend kotlinx.coroutines.CoroutineScope.() -> kotlin.Unit
): kotlinx.coroutines.Job { /* compiled code */
block()
}
It is the launch
function definition in the Kotlin resources. If we observe closely at this function:
Coroutine builders are extension of a Coroutine Scope.
Firstly, you will notice that it is an extension function from Coroutine Scope so we can only use it with indicating scope.
So, what is scope?
Scopes represent a structured environment that manages the lifecycle and execution of coroutines. For example, if you work on ViewModel, you must use insideviewModelScope
it because scope manages when the coroutine’s life will end. If you stick the coroutine to scope when the ViewModel destroys, the coroutine will also destroy. It looks more manageable because of coroutine sizes and does not cause memory leaks.
Coroutine builders can be considered a bridge between the normal and the suspending world.
As you see, the block()
suspending function has passed as a parameter to the launch builder. block()
function corresponds to the operations you will do in the launch builder asynchronously. So, suspending functions can be used inside a coroutine builder.
Coroutine builders are not suspending themselves so they can be considered a bridge between the normal and the suspending world.
“launch” function returns a Job object .
We said that we don't have to get results with the launch
builder, you see it returns the Job
object. It can confuse you, but the Job
object isn't a result of the operation inside the coroutine block. It is used for tracking the execution. It means that Job
can be used to manage and cancel the coroutine if necessary.
CoroutineContext and Start
The launch
builder passes CoroutineContext, an interface containing a set of element instances like Job
, CoroutineName
, CoroutineDispatcher
, etc.
We want to touch on another point the actual execution of a coroutine can 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.
Usecases of launch
It is suitable for cases where you need to perform background operations without waiting for a specific outcome. For instance, launching a coroutine can be useful for logging operations, such as writing logs to a file or sending logs to a remote server, asynchronously without blocking the main thread. Similarly, it can be utilized for analytics tracking, allowing the app to track events asynchronously, such as recording user interactions or monitoring app usage statistics.
In my hypothetical world: Let’s concert with
launch
!
Let's assume that we are in an instrumental concert. The musicians start playing and they will probably play at the same time(asynchronously) :) You can prefer the launch builder for the execution of their tasks because the concert occurs instantaneously(doesn’t need to wait for a result) it is only execution that you need to play concurrently.
Are they saving something and need a result of their music? No, they only pressed a note and forgot.
import kotlinx.coroutines.*
fun main() = runBlocking {
println("The music started: ${Thread.currentThread().name}") // main thread
launch { makeMusic("vocalist") }
launch { makeMusic("drum") }
launch { makeMusic("guitarist") }
println("The music is flowing: ${Thread.currentThread().name}") // main thread
}
suspend fun makeMusic(musician: String){
println("${musician} start playing: ${Thread.currentThread().name}") // Thread: T1
delay(2000)
}
As you know, the launch function returns a Job
object, which can be used to manage and cancel the coroutine if necessary. You can assume that every player's execution is a job and Maestra sometimes cancels the drum whenever Maestra wants to start a new coroutine for the drum if Maestra wants to continue.
Also, the launch
is an extension function from CoroutineScope
so it must be performed in a scope you can keep in mind is a scope that concert area.
Get a result asynchronously with “async”
The async
coroutine builder is used to start a new coroutine that performs computation and returns a computation’s result which is called a Deferred
.
We said Deferred
is a result but it's better to say it’s an asynchronous operation result’s promise. What did I mean by “promise”?
If we consider the async
builder's working mechanism, the async
doesn't trigger the coroutine directly without calling the await()
. So you need theawait()
function to start and retrieve a promised result.
import kotlinx.coroutines.*
fun main() = runBlocking {
val deferredResult: Deferred<String> = async {
delay(1000L)
"World!"
}
println("Hello, ${deferredResult.await()}")
println("Next step")
}
You will see the first output “Hello World!”. After “Hello World!” output, you will see the “Next step” output because of waiting for the deferred result.
Also, please try removing await(), you’ll notice the async function never works.
You may be confused, but let’s continue with our theoretical sides.
Anatomy of the “async”
public fun <T> kotlinx.coroutines.CoroutineScope.async(
context: kotlin.coroutines.CoroutineContext = COMPILED_CODE,
start: kotlinx.coroutines.CoroutineStart = COMPILED_CODE,
block: suspend kotlinx.coroutines.CoroutineScope.() -> T
): kotlinx.coroutines.Deferred<T> { /* compiled code */
block()
}
It is the “async” function definition in the Kotlin resources. If we observe closely at this function:
The “async” function is an extension of a Coroutine Scope.
Such as the launch
, the async
function is also an extension of the Coroutine Scope. So we can only use it with indicating scope.
The “async” function returns a Deferred object .
It is the main difference between the launch
and async
functions that the async
function returns a Deferred
value.
What is the Deferred? In other words, promised?
The deferred object is the equivalent of Future
or Promise.
It corresponds asynchronous operation result’s promise.
public interface Deferred<out T> : kotlinx.coroutines.Job {
public abstract val onAwait: kotlinx.coroutines.selects.SelectClause1<T>
public abstract suspend fun await(): T
@kotlinx.coroutines.ExperimentalCoroutinesApi public abstract fun getCompleted(): T
@kotlinx.coroutines.ExperimentalCoroutinesApi public abstract fun getCompletionExceptionOrNull(): kotlin.Throwable?
}
If we examine Deferred
closely;
Firstly, you will see that it comes from theJob
. Because you still need to manage coroutine execution.
Secondly, you will notice that you need to use Deferred<T>
, T
is your object which is a result of the async operation. Example of the above returns Deferred<String>
which is output “World!”
Also, you will notice the await()
suspend function on the deferred interface which is used for waiting and getting the result.
Retrieve the deferred result of asynchronous operation with
await()
Unlike launch
, async
function doesn't start the coroutine immediately(lazy execution). It only starts the coroutine when you explicitly call await()
on the associated Deferred
object.
This await()
function ensures waiting for the deferred
value and not moving on to the next step before the “async” builder is executed.
Usecases of the “async”
async
is beneficial when you need to perform computations concurrently and require the result for further processing. It is suitable for scenarios like parallelizing two processes, like getting data from two different places, to combine them.
For instance, imagine you’re developing a weather application that needs to fetch weather data for multiple cities concurrently. In this scenario, using async
allows you to initiate multiple API requests concurrently, significantly reducing the overall response time.
Another example could be processing large datasets in parallel. Suppose you’re developing a data analysis tool that needs to perform complex computations on different segments of a dataset concurrently. Utilizing
async
enables you to distribute these computations across multiple coroutines, maximizing the utilization of available CPU cores and improving the overall performance of your application.
Let’s take a song record with
async
:)
At this time, I want to make a song record together ;) For the record, we need to get each of a musician's plays and combine them to create a song. You can assume the deferred value as an instrument record which corresponds “${musician}’s music “. Let’s record our music.
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
fun main() = runBlocking {
println("The music started: ${Thread.currentThread().name}") // main thread
val time = measureTimeMillis {
val deferredResult1 = async { takeArecord("vocalist") }
val deferredResult2 = async { takeArecord("drum") }
val deferredResult3 = async { takeArecord("guitarist") }
val song = deferredResult1.await() + deferredResult2.await() + deferredResult3.await()
println("The song is ${song}: ${Thread.currentThread().name}") // main thread
}
println("The song created in ${time} milliseconds")
}
suspend fun takeArecord(musician: String): String{
println("The song is creating by ${musician}: ${Thread.currentThread().name}")
return "${musician}'s music "
}
With pressing play on Kotlin playground, you will notice each operation will occur in a different coroutine it is perfect and less time-consuming.
To combine all the song records we need to call await()
function. Unless all deferred results work, the next instruction won’t work because of the await() function waiting for each record result.
Also, you can manipulate the code here and try different cases with the async builder.
Block the current thread with “runBlocking”
The runBlocking
coroutine builder creates a new coroutine on the current thread and blocks the current thread until all the coroutines inside it are completed. What does the “block” mean to you?
The runBlocking
has a sequential working mechanishm.
The launch
and async
builders' main purpose is not to block the current thread and work concurrently. Unlike launch
and async
builders, runBlocking
works sequentially means that it waits for a result and doesn't jump to the next step before the previous operation finishes so it blocks the current thread.
So, why do we need runBlocking
if we are focusing on avoiding blocking the main thread?
The runBlocking is a bridge between the normal and suspending world
Calling a suspending function from a “normal” function directly cannot compile. Coroutine builders allow us to call the suspend function although they can be called from the normal function. But, the launch
and async
builders need to be used inside CoroutineScope
unlike runBlocking
.
The runBlocking
works synchronously, you can use it directly without scope you can also use suspending functions inside it. It is designed to bridge between regular synchronous code and suspending code in the context of the main function.
import kotlinx.coroutines.*
fun main() {
println("Main program starts: ${Thread.currentThread().name}") // main thread
runBlocking { // main thread
println("Fake work starts: ${Thread.currentThread().name}")
delay(1000)
println("Fake work finished: ${Thread.currentThread().name}")
}
println("Main program end: ${Thread.currentThread().name}") // main thread
}
Anatomy of the “runBlocking”
As you can see from its signature, the function passed to runBlocking
is a suspending function, even though runBlocking
itself is not suspending.
public fun <T> runBlocking(
context: kotlin.coroutines.CoroutineContext = COMPILED_CODE,
block: suspend kotlinx.coroutines.CoroutineScope.() -> T
): T {
contract { /* compiled contract */ }; /* compiled code */
}
Usecases of the “runBlocking”
TherunBlocking
is primarily used in test code, examples, and main functions to create a new coroutine and block the current thread until its completion. It's a convenient way to write synchronous-style tests for asynchronous code, making testing easier. Keep in mind that runBlocking
isn’t recommended for production code.
In conclusion, you can assumerunBlocking
because of the blocking side, it’s similar to a single performance maybe :)
See you soon :)