8.5.2 Implementation of Coroutines

Implementation of Coroutines

Here is a basic example of a function built with coroutines:

val job = GlobalScope.launch {
    delay(1000L)
    println("World!")
}
println("Hello,")
job.join() // waits until the job is completed before it outputs 

// Output: 
// Hello, 
// World 

As we see from the example, job is a coroutine that we declare first, but it does not output until we call job.join(). This allows us to run functions ahead of time and only output them when needed.

The issue with the code above is that it uses GlobalScope, which acts like a top-level thread. If the code inside of the GlobalScope consumes a lot of resources, we don't want the operation to run at the top-level in case it blocks the rest of the app from functioning. Instead, we want to be using CoroutineScope.

In the example below, we'll present a code block that uses CoroutineScope as well as two different functions launch {...} and runBlocking {...}. Both runBlocking and coroutineScope will create a new coroutine scope and waits for its children to finish before completing; however, the difference is that runBlocking will block operations until it finishes and coroutineScope only suspends, thus allowing other functions to use the thread.

fun main() = runBlocking { // Creates coroutine scope A
    launch { // Creates coroutine scope B
        delay(200L)
        println("Task from runBlocking")
    }
    
    coroutineScope { // Creates a coroutine scope C
        launch { // Creates a coroutine scope D
            delay(500L) 
            println("Task from nested launch")
        }
    
        delay(100L)
        println("Task from coroutine scope") 
    }
    
    println("Coroutine scope is over") 
}
// Output: 
// Task from coroutine scope
// Task from runBlocking
// Task from nested launch
// Coroutine scope is over

In the example above, we can see that the runBlocking forces the app to block on main() until all its operations are completed. We can also see that code from coroutine scope C starts to run before suspending and allowing the code within coroutine scope B to complete and returning to the nested coroutine scope within C.

Coroutines are extremely powerful in this sense because you can theoretically run thousands of coroutines at the same time, and the app won't crash.

suspend fun main() {
    coroutineScope {
        repeat(1_000) { 
            launch { functionA() }
        }
    }
}

// Output: 
// functionA will run 1000 times. 

Implementation with Suspending Functions

Generally, we want to be creating suspending functions to free up space in memory and allow the processors to determine which functions to complete first (unless there is a need for blocking functions). Below is an example on how to build a suspending function:

fun main() = runBlocking {
    launch { functionA() }
    println("Hello,")
}

suspend fun functionA() { // keyword suspend is important
    delay(1000L)
    println("World!")
}

// Output: 
// Hello, 
// World!

In the example above, we use the keyword suspend to specify our suspending function. We can see from the example that code in the outer scope of runBlocking is ran first before the code inside of launch due to suspension.

Last updated