8
votes

I have tried reading various tutorials and pages on Kotlin coroutines and even though it kind of makes sense to me, I still don't feel it has clicked and I dont feel ready to jump on writing async non-blocking code with coroutines. I think what i am missing is a diagram or picture of what exactly happens and in what order when a piece of coroutine code executes. How does that code run at the thread level?

    launch {
        delay(1000)
        println("World (${currentThread().name})")
    }
    println("Hello (${currentThread().name})")
    sleep(1500)

My understanding is this. I am happy to be corrected or given a different example to further my understanding.

Line0: Code starts on main thread

Line1: Launches a new coroutine on a new thread (from forkjoin pool i suppose)

Line2: Suspending function so the coroutine suspends and returns the thread to the thread pool (hence being non-blocking)

Line5: Prints on the main thread

Line6: Blocks the main thread for 1.5s

Line3: The coroutine resumes execution on (not sure which thread here - same as the thread before suspension or can be a different thread?). The coroutines prints on that thread and finishes, hence returning the thread to the pool again.

Another question i have is how would the low-level execution change if i wrap the whole code around runBlocking { ... }

2
To better understand what is behind, I would recommend watching this InfoQ presentation. It is about ongoing work in Oracle targeted to offer continuations/fibers in Java. Conceptually it is pretty much like Kotlin co-routines.yegodm
It helps to think of "launch" as a traditional thread creation in Java, which is to say, works independently of the main thread that created it. runBlocking means the coroutine and all coroutine it creates must first end. Applied to a main, it means the program doesn't end unless all spawned coroutines have finished (unlike a daemon).Neil
@Neil runBlocking does much more than that, it creates an environment for coroutines to run in. It isn't even supposed to be used once you're already within a coroutine environment. The fact that it doesn't end before its coroutines complete is just a side-effect of this.Marko Topolnik
@MarkoTopolnik I agree, but it is a bit much to explain all this in a single comment. I wanted to distinguish java-like behavior from the newer coroutines in kotlin, as he's probably coming from a Java background.Neil
@Neil I see what you mean. I'd still point out that launch doesn't typically work independently of the main thread that created it (the basic idiom on Android is to let it work on that same thread), and that this is pretty much exactly what makes coroutines so different from threads. You must jump through this cognitive hoop, accept that there is now concurrency on a single thread. This is key to really digging coroutines.Marko Topolnik

2 Answers

6
votes

Your code doesn't actually do anything that would reveal the special nature of coroutines. It makes two threads do their thing concurrently, just as they would do it in plain Java.

It only gets interesting when you launch a coroutine on the same thread you're already on (for example, the main thread). This is one of the things you achieve with a runBlocking block:

runBlocking {
    launch {
        delay(1000)
        println("Hello from the launched coroutine. My thread is "
                + Thread.currentThread().name)
    }
    println("Hello from the top-level coroutine. My thread is "
            + Thread.currentThread().name)
}

This will print

Hello from the top-level coroutine. My thread is main
Hello from the launched coroutine. My thread is main

runBlocking runs an event loop on the calling thread and propagates a reference to it to all the coroutines you launch within it. For example, delay(1000) will post an event to this event loop, with an indicated delay of one second, and then it will suspend the coroutine. This will allow the main thread to run the rest of the code below launch. When the time elapses, the event loop will run the event handler, which will in turn resume the coroutine.


An even more educational example is launching a coroutine without any Dispatcher. This removes the illusion of coroutines looking like threads and reveals their true magic: by calling continuation.resume() you make the current thread jump right into the middle of the suspended coroutine's block of code, and all that happens using plain Java method calls. I suggest studying this answer where this is explained in detail. If you're interested in an even more in-depth explanation of how plain Java methods can do this trick, I suggest you watch Roman Elizarov explaining this on YouTube.

1
votes

Here is another example metioned here: https://kotlinlang.org/docs/reference/coroutines/basics.html#your-first-coroutine

fun main() {
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
        println("World!") // print after delay
    }
    println("Hello,") // main thread continues while coroutine is delayed
    Thread.sleep(2000L) // block main thread for 2 seconds to keep JVM alive
}

The above code prints:

Hello,
World!

As you can see, although the code looks synchronous, the execution is asynchronous. That's the whole idea of Coroutine.