Questions about concurrency?

I’ve read about suspending functions, coroutines, channels and flows in kotlin docs.

Are coroutines something like green threads, i.e. they aren’t necessarily native threads but have a m:1 or probably m:n mapping to native os threads?

Is calling a suspending function issueing a context switch of the parent coroutine to execute the next (top-down) child coroutine unless the current coroutine doesn’t share the same dispatcher as the parent coroutine?

Does having suspending functions imply that Kotlin is cooperatively (green) threaded?

Or are there opportunities to suspend preemptively executions?

Without suspending functions, are both coroutines guaranteed to run sequentially or is this dispatcher dependent?

    launch {
        println("World 1")
    }
    launch {
        println("World 2")
    }

Yeah, coroutines are green threads. They’re units of work that can be scheduled onto OS thread pools, the work can be cancelled, and they enforce structure such that even if an exception is thrown in one thread pool, the caller will receive the exception and stop any other child jobs so you don’t have lingering concurrent resources.

Roman Elizarov did a great talk about the motivation behind coroutine design and how it works under the hood, you can find it here: GOTO 2019 • Server-side Kotlin with Coroutines • Roman Elizarov - YouTube

3 Likes

But to directly answer your questions:

Are coroutines something like green threads, i.e. they aren’t necessarily native threads but have a m:1 or probably m:n mapping to native os threads?

Yes. The default dispatcher spawns a number of Java threads in a thread pool equal to the number of CPU cores on your machine which it schedules coroutines onto, an M:N mapping.

Is calling a suspending function issueing a context switch of the parent coroutine to execute the next (top-down) child coroutine unless the current coroutine doesn’t share the same dispatcher as the parent coroutine?

When you invoke a suspending function from another suspending function, it inherits the context of the caller unless you explicitly invoke withContext() to switch to a different one. When you call withContext() what it will do is:

  1. Suspend the calling coroutine (i.e. exit and wait for reschedule) and schedule withContext()'s lambda to be executed within the specified context, i.e. on a different thread pool if you chose a different dispatcher.
  2. The lambda executes in the specified context
  3. When the execution completes, the calling coroutine resumes, picking up immediately after the withContext() call in the original context. Kotlin coroutines automatically handle the copying of data between threads if you modified data from the surrounding scope.

Does having suspending functions imply that Kotlin is cooperatively (green) threaded?

Yes, cancellation is at least. Suspension points may cause the current coroutine to suspend and may throw CancellationException if the invoked code gets cancelled. If you’re writing CPU-intensive code with no suspension points you may need to check the isActive value of your current coroutine and exit at an appropriate time, which is available in all suspending functions.

Or are there opportunities to suspend preemptively executions?

The most you can do is call the cancel() function on a job, which will attempt to stop that job and all child jobs. This means that the coroutines running in a job will have isActive set to false, and calls such as delay() and yield() may throw CancellationException to stop the work.

Without suspending functions, are both coroutines guaranteed to run sequentially or is this dispatcher dependent?

It’s dispatcher dependent. If the dispatcher in question is backed by a single thread, the two coroutines will be scheduled on the thread sequentially. However, in the case of Dispatchers.Default it’s more likely that the two coroutines in your sample code will be concurrently scheduled onto 2 different threads in the default dispatcher’s thread pool.

Hope that helps!

2 Likes

Thanks for the detailed answers!

I thought Kotlin support shared data between coroutines?

It does. I guess I slightly misspoke - because all objects in Java are passed by reference, the references are copied into the thread calling the code in the withContext() block. However this same behavior is achieved with primitives such as Int as well. This is achieved with the continuation object under the hood, which is responsible for storing the local state of a coroutine when it is suspended. So in the following code:

suspend fun example() {
    var myNumber = 3
    withContext(otherDispatcher) {
        delay(100)
        myNumber = 4
    }
    println(myNumber)
}

What would happen upon invocation of example() is it would run in the calling context initially, then example() gets suspended when withContext() is invoked. The code inside the lambda then executes in the other context, immediately suspends for 100ms, then resumes and sets myNumber to 4. What then happens is myNumber gets copied into the continuation for resuming example().

When example() is resumed in the original context, it uses the continuation to determine that execution should be picked up immediately after the withContext() call and to rehydrate its local state. Previously myNumber was 3, but after the withContext() call, it gets set to 4 because that’s what’s stored for that symbol in the continuation. Therefore, when println() is executed it prints 4 instead of 3, even though the variable was modified on a different thread.

2 Likes

I think this might be slightly inaccurate because IIRC this will be treated just like a normal closure and so my number will actually be of type IntRef which then means that the code inside the withContext block will change the variable inside of that IntRef and then the rest of the example code will query the variable stored inside of the IntRef. Tbh tho this is all Java-Kotlin-Interal-Implementation magic that shouldn’t matter too much

2 Likes

Haha you’re right - the internal implementation doesn’t matter too much, the important thing to remember is that the withContext() scope will behave like any other lambda and you can mutate variables from the outer scope if you want. Just keep in mind that doing so concurrently may cause unexpected behaviors.

3 Likes