Kotlin Coroutines: Essential Guide to Master Asynchronous Programming
Learn How to Handle Concurrency, Optimise Threads, and Build Scalable Applications with Kotlin Coroutines
Introduction
One of the major problems we face while development, is avoiding our application to get caught in bottlenecks. This leads application to blocking states and crash.
Coroutines enables us to write non-blocking code, by helping us write functions that can suspend their execution and resume their execution, as per the application needs.
Whenever a user launches an application, the tasks associated with it are initiated on a main thread. These tasks range from small logical operations to UI interactions.
However these threads are’t suitable for running high end tasks like long operations, downloading, uploading, database queries, etc. as this may block the main thread to perform other operations and will lead the application to be slow or may even crash it.
To handle this situation, we may create more background threads also known as worker threads what will be given the task for handling heavyweight operations.
However, this isn’t a scalable solution as the number of threads are limited as they are expensive and make the system go out of memory.
Coroutines
Coroutines are light weight threads that are not as the same as threads but can run in parallel with each other, wait for each other, communicate with each other just how a thread does.
Unlike threads, Coroutines are very cheap. Multiple Coroutines can be executed on a single background thread.
To start using Coroutines in out Kotlin Project, we need to follow the following steps:
- Add the coroutine dependency in our
build.gradle
file
dependencies{
//statements..
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
- Import the Coroutine Library in the Kotlin file
import kotlinx.coroutines.*
fun main() { //statements..
}
Implementation
- Getting to know the environment :
import kotlin.coroutines.*
fun main(){
println("main starts here : ${Thread.currentThread().name}")
Thread.sleep(3000)
println("main ends here : ${Thread.currentThread().name}")
}
/*Output:
main starts here : main
main ends here : main
*/
As our main function gets executed in the main thread, we see the output as main in both the print statements, with the delay of 3 seconds.
- Creating a background(worker) thread using the
Thread
keyword and.start()
function:
import kotlin.coroutines.*
fun main(){
println("main starts here : ${Thread.currentThread().name}")
Thread{
println("bg starts here : ${Thread.currentThread().name}")
println("bg ends here : ${Thread.currentThread().name}")
}.start()
//Case1: Thread.sleep(4000)
println("main ends here : ${Thread.currentThread().name}")
}
/*Output:
main starts here : main
main ends here : main
bg starts here : Thread-0
bg ends here : Thread-0
*/
//Case1 will lead to execution in the order main,t0,t0,main
As both threads are executed in parallel, the main thread completes its execution side by side while the background thread is also executing. Order of print statements may change depending upon the delays.
- Building Coroutines using the
launch
lambda function:
import kotlin.coroutines.*
//Case 1:
fun main(){
println("main starts here : ${Thread.currentThread().name}")
GlobalCoroutine.launch{
println("c starts on t : ${Thread.currentThread().name}")
println("c ends on t : ${Thread.currentThread().name}")
}
println("main ends here : ${Thread.currentThread().name}")
}
/*Output:
main starts here : main
main ends here : main
*/
//Case 2:
fun main() {
GlobalScope.launch {
println("c starts on t: ${Thread.currentThread().name}")
delay(1000)
println("c ends on t: ${Thread.currentThread().name}")
}
println("Main thread ends: ${Thread.currentThread().name}")
Thread.sleep(500) // The main thread sleeps briefly
}
/*Output:
main starts here : main
c starts on t : DefaultDispatcher-worker-1
*/
the GlobalCoroutine.launch
function creates coroutine that is not linked to any lifecycle and is a standalone in the global scope. This also doesn’t block the thread in which it is operating. As the application doesn’t wait for the coroutine, it only waits for all the threads to end.
Thread.sleep()
block the whole thread, and all the coroutines that are being executed on it.
delay()
is a similar function that only delays the current coroutine and doesn’t affect the thread or other coroutines that run on it. However delay is s suspending function (a function built using the suspend
keyword and can only be called by a coroutine or other suspending functions)
fun main(){
//MAIN THREAD
GlobalScope.launch{
//T1 THREAD
delay(1000) //Coroutine Suspended, T1 FREE
//T1/other THREAD
}
//MAIN THREAD
}
So coming back to our previous code, we will wait for our global scope coroutine, using the delay()
function.
import kotlin.coroutines.*
fun main(){
println("main starts here : ${Thread.currentThread().name}")
GlobalCoroutine.launch{
println("c starts on t : ${Thread.currentThread().name}")
println("c ends on t : ${Thread.currentThread().name}")
}
runBlocking{
delay(2000)
}
println("main ends here : ${Thread.currentThread().name}")
}
/*Output:
main starts here : main
c starts on t : DefaultDispatcher-worker-1
c ends on t : DefaultDispatcher-worker-1
main ends here : main
*/
As delay()
is a suspending function we call it from a runBlocking
lambda function: that creates a coroutine that runs on the current thread. Hence, in this case we delay the main thread by 2 seconds, and our Coroutine gets executed.
Another way to rewrite the above code:
import kotlin.coroutines.*
fun main() = runBlocking{
//MAIN THREAD
GlobalCoroutine.launch{
//T1 THREAD
}
delay(2000) //MAIN delayed, thus T1 completed
//MAIN THREAD
}
Coroutine Builders
Coroutine Builders are functions that help us create and launch coroutines.
launch
&GlobalScope.launch
async
&GlobalScope.async
runBlocking
Using GlobalScope
Companion Object with the coroutine builders makes the scope of the coroutine to the application level rather than the context level. Lifecycle of Local coroutines gets destroyed when that local context is destroyed.
Therefore, we discourage the use of Global Scope Coroutines as if left unhandled, it may be left in the background, wasting the resources.
launch
Creates a Coroutine in the scope of the parent Coroutine Thread.
import kotlinx.coroutines.*
fun main() = runBlocking { //MAIN
//MAIN
launch { //inherits MAIN
println("Running in ${Thread.currentThread().name}")
}
//MAIN
}
This function returns an object of type Job
and hence can be used to perform inbuilt functions of Job
.
import kotlinx.coroutines.*
fun main() = runBlocking { //MAIN
//MAIN
val job : Job = launch { //inherits MAIN
println("Running in ${Thread.currentThread().name}")
}
job.join() //will make current thread wait for job to finish
job.cancel() //cancel coroutine
//MAIN
}
Use Case: Fire & Forget Tasks like updating UI, etc.
async
Creates a coroutine in the scope of the parent Coroutine Thread but also returns a result.
import kotlinx.coroutines.*
fun main() = runBlocking { //MAIN
//MAIN
async { //inherits MAIN
println("Running in ${Thread.currentThread().name}")
}
//MAIN
}
This function returns an object of type DeferredJob
which is a subclass of Job
import kotlinx.coroutines.*
fun main() = runBlocking { //MAIN
//MAIN
val jobD1 : Deferred<Unit> = async { //inherits MAIN
println("Running in ${Thread.currentThread().name}")
}
jobD1.join() //will make current thread wait for job to finish
jobD1.cancel() //cancel coroutine
val jobD2 : Deferred<Int> = async { //inherits MAIN
println("Running in ${Thread.currentThread().name}")
12 //returning value
}
jobD2.join() //will make current thread wait for job to finish
jobD2.cancel() //cancel coroutine
job2.await() //make thread wait for coroutine result
//MAIN
}
Use Case: Perform computation and await their result
runBlocking
Creates a Coroutine that blocks the current thread until the coroutine inside it completes.
Coroutine Cancellation
Coroutine Cancellation is required when its execution is too time consuming, unnecessary or undesirable. However cancelling a coroutine is only possible when the coroutine is cooperative.
val job = launch { /* */ }
job.cancel() //cancels a job if it is cooperative
job.join() //waits for the job to finish
job.cancelAndJoin()
//if the job will be cooperative, it will be cancelled.
//else it will wait for the job to finish
Two Methods of making a Coroutine Cooperative :
Periodically using suspending functions given by kotlin like
delay()
,yield()
,withContext()
, as they check for coroutine cancellation.fun main() = runBlocking{ //Main val job = launch{ //T1 for(i in 0..500){ print(i) } } job.join() //wait for T1 } //prints from 0 to 500
fun main() = runBlocking{ //Main val job = launch{ //T1 for(i in 0..500){ print(i) delay(50) //makes coroutine on T1 cooperative //yield(50) : if don't want coroutine to delay } } delay(200) job.cancelAndJoin() //wait for T1 } //prints only some numbers eg: from 0 to 23. //as coroutine cancelled after 200ms
However, when a coroutine gets cancelled with the
delay()
being responsible for it. Thedelay()
function throws a cancellation exception which is a subclass of exception.val job : Job = launch(Dispatchers.Default){ try{ for(i in 0..500){ print(i) yield(50) } delay(5) job.cancelAndJoin() } catch(e: CancellationException){ print("exception caught") } }
Explicitly check for cancellation status within the coroutine using the
CoroutineScope.isActive
boolean value.val job = launch(Dispatchers.Default){ for(i in 0..500){ if(!isActive) break print(i) } delay(10) job.cancelAndJoin() } //using //return @launch //instead of break will return too.
Timeout in Coroutines
Timeout allows us to set a time limit for the execution of a coroutine. Surpassing that time limit will automatically cancel the coroutine.
Using withTimeout()
coroutine builder :
import kotlinx.coroutines.*
fun main() = runBlocking {
try {
withTimeout(2000) { // Timeout set to 2 seconds
//statements
}
} catch (e: TimeoutCancellationException) {
println("Timeout occurred!")
}
}
//The withTimeout() functions throws a timeout cancellation
//exception if the coroutine exceeds the time
Using withTimeoutOrNull()
coroutine builder :
import kotlinx.coroutines.*
fun main() = runBlocking {
val result = withTimeoutOrNull(2000) {
//statements
"Done" // Return value if completed in time
}
println("Result: $result")
}
//returns null if the coroutine exceeds the timelimit
//and doesn't throw an exception
Composing Suspending Function
Kotlin Coroutines allows us to make suspending functions that perform task sequentially or concurrently
suspend fun task1(): String {
delay(1000)
return "Task 1 Completed"
}
suspend fun task2(): String {
delay(1000)
return "Task 2 Completed"
}
- Sequential Execution : by default tasks are executed one after another in the order they are called. Each suspending function waits for the previous one to complete before starting.
import kotlinx.coroutines.*
fun main() = runBlocking {
val result1 = task1()
val result2 = task2()
println(result1 + result2)
//three statements take total of 2000ms
}
- Concurrent Execution : tasks are started simultaneously & execute in parallel. the
async
function launches separate child coroutines for each task, so the total time taken is the time the longest task requires. We use async as we here return a result.
fun main() = runBlocking {
val deferred1 = async { task1() }
val deferred2 : Deferred<Stirng> = async { task2() }
println(deferred1.await() + deferred2.await())
}
- Lazy Coroutine Execution : we can use the same concept for
launch
andasync
val lazyJob1 = async { task1() }
val lazyJob2 = async { task2() }
//task1() & task2() will execute even if we don't use
//lazyJob1 & lazyJob2
val lj1 = async(start = CoroutineStart.LAZY) { task1() }
val lj2 = async(start = CoroutineStart.LAZY) { task2() }
//task1 task2 won't execute
print(lj1 + lj2) //task1 & task2 will execute now
Coroutine Scope
Each coroutine has its own scope in Kotlin. When inside a coroutine scope, the this
keyword refers to the current scope of the coroutine.
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Current scope: $this") // Refers to runBlocking's CoroutineScope
launch {
println("Child scope: $this") // Refers to the CoroutineScope of launch
}
}
//Output:
//Current scope: BlockingCoroutine{Active}@<hashcode>
//Child scope: StandaloneCoroutine{Active}@<hashcode>
Coroutine Context
Set of elements that define the coroutine. Each coroutine has a unique coroutine scope but multiple coroutine can have the same coroutine context.
Components :
Dispatcher
: determines the thread that the coroutine uses. a)Dispatchers.Default
: optimised for CPU intensive tasks b)Dispatchers.IO
: optimised for I/O tasks c)Dispatchers.Main
: optimised for UI tasks on the main thread d)Dispatchers.Unconfined
: starts coroutine in current threadJob
: represents the coroutine and allows cancellation, modificationCoroutine Name
fun main() = runBlocking{ //MAIN
//Without Parameter :
//inherits context of parent coroutine
//even after delay, runs on same thread
launch{
//MAIN
delay(1000)
//MAIN
}
//With Parameter :
//gets it own context at global level,
//gets seperate background thread
//after delay, may get another thread
launch(Dispatchers.Default){
//T1
delay(1000)
//T1 or other
}
//With Parameter :
//inherits context of parent coroutine
//after delay, gets another thread
launch(Dispatchers.Unconfined){
//main
delay(1000)
//T1
}
//using coroutineContext :
//inherits context of parent coroutine
//even after delay, runs on same thread
launch(coroutine){
//MAIN
delay(1000)
//MAIN
}
}