Getting familiar with Kotlin Coroutines
What is a coroutine?
At this point, if you're researching for coroutine knowledge, I assume you already know what a Thread is; so, there is no simple way to describe a coroutine better than the definition found on the Kotlin official website
```A?coroutine?is an instance of suspendable computation. It is conceptually similar to a thread in the sense that it takes a block of code to run that works concurrently with the rest of the code. However, a coroutine is not bound to any particular thread. It may suspend its execution in one thread and resume in another one.```
Nevertheless, if you have ever worked with threads before, it's time to let you know. Because working with coroutines in a real-life might looks way different than working with threads.
fun main() {
runBlocking {
val job = launch {
println("I don't know what to do!")
delay(300)
println("I know what to do now, but it's too late =( ")
}
delay(100)
println("Time is over!!!")
}
}
It's time to dive deep!
CoroutineContext
Every coroutine has what we call a context where it holds all the information the system needs to run it correctly, The most common "Elements" you'll find in there would be.
There are many others, but we should stick to those for now.
Job
So far, we could see that using a coroutineBuilder like `launch` produces a job, and we know that a Job has a life cycle, but we didn't mention so far that is that this Job can have states.
To understand a job execution, we must master its states to know what to do with it.
A coroutine, when launched, always starts as Active unless you add more parameters to the builder `start = CoroutineStart.LAZY `, after completing its work, the Job passes to the Completing state where it'll wait for all its children to finish (Don't worry, we will talk about the parent-child relationship soon)
In case of exceptions, the Job is moved to a Cancelling state where it will await for all children to complete its cancellation execution.
After all the children have finished or are cancelled, the?Job?moves to either the?Completed?or?Cancelled?state.
Parent-child relationship
A job becomes a child of this Job when it is constructed with this Job in its?CoroutineContext?or using an explicit?parent?parameter.
A parent-child relationship has the following effect:
Here is a quite simple example of a parent coroutine with its children.
领英推荐
fun main() {
runBlocking {
val parent = launch {
val child1 = launch {
println("I'm child 1")
}
val child2 = launch {
println("I'm child 2")
}
val child3 = launch {
println("I'm child 3")
}
}
}
}
Dispatchers
Dispatchers are responsible for helping the coroutines decide in what thread or threads the corresponding coroutine will be executed. A dispatcher can confine coroutine execution to a specific thread, a thread pool or let it run unconfined.
fun main() {
runBlocking {
launch(Dispatchers.Default) {
println("Executing on the Default Dispatcher")
async(Dispatchers.IO) {
delay(300)
println("Heavy operations on the IO Dispatcher")
}
}
}
}
There are four main dispatchers, and you definitely must understand how they work to get your application running in good shape.
Main?Dispatcher: It starts the coroutine in the main thread, and, It is mostly used when performing UI operations within the coroutine.
IO Dispatcher: uses a shared pool of on-demand created threads and is designed for offloading of IO-intensive?blocking?operations (like file I/O and blocking socket I/O).
Default Dispatcher: It's the default dispatcher used when no dispatcher is specified in the builder, such as Launch an Async. It uses a shared pool of threads on JVM, and the maximum number of threads used by this dispatcher is equal to the number of CPU cores.
Unconfined Dispatcher: A coroutine dispatcher that is not confined to any specific thread. It executes the initial continuation of a coroutine in the current call-frame and lets the coroutine resume in whatever thread that is used by the corresponding suspending function without mandating any specific threading policy.
ExceptionHandler
As we all know, every code may lead to an exception, and coroutines are not different, but thinking on that, coroutines have pleased us with an Element that facilitates it inside its context. Which is called ExceptionHandler
There are a few things you must know when handling exceptions.
Side note: Normally, the handler is used for logging the exception, showing some error message, and terminating and/or restarting the application. Usually, no logic is appended there.
Here is an example of a custom coroutineHandler:
fun main() {
val handler = CoroutineExceptionHandler { _, exception ->
println("Handle $exception in CoroutineExceptionHandler")
}
val scope = CoroutineScope(newSingleThreadContext("Test Scope") + handler)
runBlocking {
scope.launch {
val child = launch {
throw Exception("Error inside the child coroutine")
}
}.join()
}
}
Structured Concurrency
Every time we talk about concurrency in software development, a few goosebumps come along; however, in coroutines, what this pair of words means is quite simple.
Structured concurrency is a way to ensure no resources are leaked, and all coroutines are accounted for and executed properly following their life-cycle. One great example of it comes when you have multiple async executions inside a scope, and one of these executions fails. You do not need to handle the exception manually in any other coroutine sharing the same context because the propagation of it will follow the parent-child relationship we have seen before.
With that said, hopefully, you will have gained basic knowledge of coroutines and will be ready to jump into real-world applications using it. See you soon!