This post is written to dictate some opinionated explanation that dispels my confusion to Kotlin coroutines during learning.

suspend keyword colors a function

The suspend keyword in Kotlin colors a function, just like the async keyword in Javascript and Python, to inform the compiler that it might execute some asynchronous tasks.

suspending functions, such as the delay() function, must only be invoked within another suspending function.

suspend fun foo() {
delay(1000)
}
fun bar() {
delay(1000) // not allowed
}
async def foo():
await asyncio.sleep(1)

def bar():
await asyncio.sleep(1) # not allowed

Invocation of suspending functions are sequential by default

If two suspending functions fun1() and fun2() are invoked one followed with another, their invocations are sequentialized, i.e., only after fun1() finished will fun2() be started. This is in contrast with Javascript or Python, where async-functions must be explicitly await-ed to sequentialize.

suspend fun fun1() { delay(1000) }
suspend fun fun2() { delay(2000) }
suspend fun foo() {
fun1()
fun2()
// foo() finished in 3 seconds
}
async def fun1(): await asyncio.sleep(1)
async def fun2(): await asyncio.sleep(2)
async def foo():
fun1()
fun2()
# foo() returns immediately

In both of the preceding codes, fun1() and fun2() are invoked without additional syntax constructs. In Kotlin the invocations are sequential by default, and thus foo() will return in 3 seconds. In Python, the coroutines created are leaked without any execution, causing foo() to return immediately, and hopefully you will get a RuntimeWarning: coroutine 'fun1' was never awaited as alert. To equalize them, the Python version should use explicit await:

suspend fun fun1() { delay(1000) }
suspend fun fun2() { delay(2000) }
suspend fun foo() {
fun1()
fun2()
// foo() finished in 3 seconds
}
async def fun1(): await asyncio.sleep(1)
async def fun2(): await asyncio.sleep(2)
async def foo():
await fun1()
await fun2()
# foo() returns in 3 seconds

Such difference between Kotlin and other languages prevents the case of developers forgetting to write await and leaking coroutines, which is a foundation of ergonomic structural concurrency.

suspend function scope != coroutine scope

In Kotlin, coroutine builders such as launch() or async() must be invoked within a coroutine scope, since they are effectively extension methods of class CoroutineScope. However, a suspend function does not essentially form a coroutine scope, which means the following code is invalid:

suspend fun foo() {
launch { delay(1000) }
}

Instead, one should wrap the function with a coroutineScope call to make use of the extensions:

suspend fun foo() {
suspend fun foo() = coroutineScope {
launch { delay(1000) }
}
suspend fun foo() {
launch { delay(1000) }
}
suspend fun foo() = coroutineScope {
launch { delay(1000) }
}

Apart from coroutineScope(), other functions like launch(), async() or runBlocking() also create coroutine scopes.

Coroutine scope is the effective way to structural concurrency

Coroutine scopes are hierarchical. When calling .launch() or .async() from a outer scope, a child scope is inherited and created from it, implicitly forming a tree-like invocation structure.

fun main(): Unit = runBlocking(CoroutineName("A")) {
launch(CoroutineName("B")) {
launch(CoroutineName("C")) {
delay(1000)
println(2)
}
println(1)
}.join()
launch(CoroutineName("D")) { println(3) }
}

In the above program, we explictly attributes the name of different coroutines in order to refer them crystally. The program will create a conceptual hierarchy as below

A --> B -> C
\-> D

Such hierarchy constrains the lifecycle of coroutines and therefore derives the structural concurrency. Specifically, we have –

Coroutine scopes are self-contained. A parental coroutine always waits for completion of all its children. Cancelling a parental coroutine recursively cancels all its children. The rules implies the program above would output like:

1 (after one second)
2
3

Since launch(CoroutineName("B")) {...}.join() would block until its child coroutine-C returns in one second. The self-containing property enables ergonomic cooperative job cancellation and ensures coroutines won’t be readily leaked 1.

  1. However, they could be leaked if, for example, you insist to use GlobalScope.launch.

Author: hsfzxjy.
Link: .
License: CC BY-NC-ND 4.0.
All rights reserved by the author.
Commercial use of this post in any form is NOT permitted.
Non-commercial use of this post should be attributed with this block of text.