how does the method of resume works or dispatcher works in the coroutines of Kotlin, which lead to different output sequence

Viewed 979

Below are two snippet, they are almost the same, except one has thread in suspend coroutine and another not.

The first code is:

suspend fun main(args: Array<String>) {
    log(0)

    coroutineScope {
        log(1)
        val result = suspendCoroutine<String> { continuation ->
            log(2)
            Thread {
                log(3)
                continuation.resume("cup")
                log(4)
            }.start()
        }
        log("5 $result")
    }

    log(6)

    Thread.sleep(100)
}

Its output is:

    [main] 0
    [main] 1
    [main] 2
    [Thread-0] 3
    [Thread-0] 5 cup
    [Thread-0] 6
    [Thread-0] 4

and second code is:

suspend fun main(args: Array<String>) {
    log(0)

    coroutineScope {
        log(1)
        val result = suspendCoroutine<String> { continuation ->
            log(2)
            // we don’t use thread in suspend coroutine
            // Thread {
                log(3)
                continuation.resume("cup")
                log(4)
            // }.start()
        }
        log("5 $result")
    }

    log(6)

    Thread.sleep(100)
}

Its output is:

    [main] 0
    [main] 1
    [main] 2
    [main] 3
    [main] 4
    [main] 5 cup
    [main] 6

and the log function is:

fun <T> log(t: T) {
    println("[${Thread.currentThread().name}] ${t.toString()}")
}

Why one’s output sequence is 0->1->2->3->5->6->4, and another is 0->1->2->3->4->5->6。

3 Answers
suspendCoroutine<String> { continuation ->
    log(2)
    log(3)
    continuation.resume("cup")
    log(4)
}

When you resume the continuation while still within the suspendCoroutine call, no suspension actually happens and suspendCoroutine simply returns the value you resumed it with. So in this case the execution order is completely sequential and deterministic.

val result = suspendCoroutine<String> { continuation ->
    log(2)
    thread {
        log(3)
        continuation.resume("cup")
        log(4)
    }
}
log("5 $result")

Here suspension actually happens because suspendCoroutine completes while the continuation is not yet resumed. This means that the dispatcher is free to run other coroutines and true concurrency ensues. You may get several different orderings, depending on timing. The ordering 2-3-4-5 is also possible, as well as what you observed.

The first thing to note is that the result will be different with runBlocking.

fun main() = runBlocking { ...

With runBlocking, the coroutine will be confined into the event loop.

Then by calling continuation.resume, the created thread will hand the result to the coroutine, which will get executed by the main thread. And 4 and 5 6 will be logged concurrently. It can be 4 5 6, 5 4 6, or 5 6 4.


suspend fun main does not define any Dispatcher, so when the Continuation is resumed then it uses the current thread. There is not thread constraints.

https://github.com/Kotlin/kotlinx.coroutines/issues/1591#issuecomment-537421694

So this is like launching a coroutine in Unconfined.

Two things can happen when you resume the coroutine. Let's look at the definition of suspendCoroutine.

public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T =
    suspendCoroutineUninterceptedOrReturn { c: Continuation<T> ->
        val safe = SafeContinuation(c.intercepted())
        block(safe)
        safe.getOrThrow()
    }

The getOrThrow in main, and the reusme in the new thread will race to set the result field of the SafeContinuation.

If getOrThrow wins, the coroutine will be suspended. The new thread will execute the rest of the coroutine by itself (because it is unconfined). It can only log 4 after running the continuation.
I.e. the result is 5 6 4.

If resume wins, getOrThrow will return the actual result, and the coroutine will not be suspended. The new thread does not try to run the continuation, because the main thread will keep on executing the rest of the coroutine.
Again, 4 and 5 6 will be logged concurrently.

You can make resume win by sleeping main after starting the new thread.


If you have looked at the code and understood the previous paragraphs. I hope it is now obvious why the result is always 4 5 6 when you resume synchronously inside suspendCoroutine. Because resume is always called before getOrThrow.


I think it is possible to use Kotlin coroutines proficiently without understanding any of the above. Just use confined coroutines and be done with it.

Because there are no guarantees about the order in which coroutines that run in parallel are executed.

Continuation.resume specifies the following behaviour:

Resumes the execution of the corresponding coroutine passing value as the return value of the last suspension point.

It does not specify when or where it will execute that code. What you are seeing are optimizations where code is executed directly in the resuming thread if that does not violate other guarantees for performance reasons.

As an exercise you can go through all the suspensions in your code and derive the following rules that any output must follow:

  1. log(0) first
  2. log(6) after log(5).
  3. log(1) after log(0).
  4. log(2) after log(1)
  5. log(3) after log(2)
  6. log(5) after continuation.resume (Resume causes suspendCoroutine to emit a return value)
  7. log(4) after continuation.resume

There is no requirement that log(4) executes before log(5) or log(6) through the order of suspending operations, so its entirely implementation-dependant.

Related