2

From the kotlin docs

A coroutine is an instance of suspendable computation. 
It may suspend its execution in one thread and resume in another one.
delay is a special suspending function. 
It suspends the coroutine for a specific time. 
Suspending a coroutine does not block the underlying thread, but allows other coroutines to run and use the underlying thread for their code.

When a coroutine is suspended the thread which was running it is free to execute some other coroutine. For example when you use delay() or callSomeAPI() they are done asynchronously.

I come from javascript world. There functions like setTimeout() or fetch() were executed outside of javascript callstack and inside the browser environment.

But in our kotlin case who exactly is executing those methods? Shouldn't there be a thread which manages stuff? So do we start a new thread to take care of the async code in coroutines?

Charan Sai
  • 105
  • 2
  • 9
  • 1
    Possibly of interest would be this article: https://www.infoq.com/articles/kotlin-coroutines-bottom-up/ - coroutines involve creating a "continuation" which stores the state of execution. This continuation can be passed to different threads (which may be part of a thread pool) – UnholySheep Nov 06 '21 at 20:06
  • Is asynchronous in the context of threads or application? Like we say `delay()` is non-blocking call. But the all the code which follows it will be sent as `continuation` means blocked till delay is done. So why exactly it is known as asynchronous? – Charan Sai Nov 06 '21 at 20:18
  • It's asynchronous because it doesn't block the entire rest of the code from running while the coroutine is waiting for the `delay` to finish. The code where the coroutine was created continues execution – UnholySheep Nov 06 '21 at 20:25
  • 1
    I think "asynchronous" is very misleading here. Suspend functions are **synchronous** by design. This is their main goal: to provide performance benefits of asynchronous code by keeping the source code synchronous. I guess people tend to call suspend functions asynchronous, because this term has much more broad meaning. We can say suspend function is asynchronous to the invoking thread, but synchronous to the invoking coroutine/function. – broot Nov 06 '21 at 20:44
  • Check out [this answer](https://stackoverflow.com/a/48112934/1103872). It has a small, self-contained example that shows you both sides -- suspending a coroutine, as well as managing the behind-the-scenes resumption, all within a single `main` function and a single thread. Everything uses this same fundamental mechanism, it just performs the `continuation.resume()` call somewhere else. – Marko Topolnik Nov 08 '21 at 11:48
  • Understanding coroutines involves understanding the concept of passing `continuations`, which are just callbacks, using a state machine. A very good explanation of these concepts is in a video called "Deep Dive into Coroutines on JVM by Roman Elizarov", one of the main authors of Kotlin coroutines. https://www.youtube.com/watch?v=YrrUCSi72E8 – Alexei Fando Jan 29 '22 at 02:16

2 Answers2

2

It really depends on what is the reason to suspend. For example, if we suspend to wait for some IO, then it is probable that underneath another thread is used to block on this IO. Some IO are event-based, so OS notifies our application that the coroutine could be resumed. But in many cases we suspend waiting for another coroutine, for example we wait for its completion or we wait on reading from/writing to a Channel (suspendable queue). Then, there is no need for additional thread - other coroutines resume our coroutine. delay() is another example. I'm not sure how does it work internally, it definitely depends on the target platform, but I don't suspect it to do busy-waiting ;-) I guess there is some kind of a timer event provided by the OS.

So once again: it depends.

broot
  • 21,588
  • 3
  • 30
  • 35
  • is there any case where OS is not handling it? If OS is not handling that means application is. – Charan Sai Nov 06 '21 at 20:14
  • @CharanSai If I'm not mistaken, coroutines are a many-to-many thread model. There is an underlying OS thread pool (typically more than one such pool), but then the coroutines are managed at the application level. Which means the coroutines are scheduled at the application level and assigned OS threads as needed, and then the OS threads run when the OS scheduler says they should. – Slaw Nov 06 '21 at 20:20
  • I'm not sure what do you mean. Coroutines are implemented entirely on top of what JVM provides in its stdlib (or another stdlib/API in the case of other targets). Coroutines don't use OS features directly. For example, I believe `delay()` is implemented on JVM using `ScheduledExecutorService` which provides delaying feature. I guess when targeting JS they use `setTimeout()` or similar API. Coroutines don't really know or care how these stdlib features work. But most probably, internally they ask OS for some kind of a timer functionality. @CharanSai – broot Nov 06 '21 at 20:21
  • Yes that's true. A coroutine is not bound to any thread. It can be suspended and when it is resumed a different thread can execute it. But what exactly is happening at suspension? Who is handling it? In case of network calls or delay() the OS is. But is it true for all cases? – Charan Sai Nov 06 '21 at 20:21
  • Internally they work similar to Java executors. Actually, in many cases when on JVM they really use executors. Whenever a coroutine is meant to be resumed, it is scheduled using some executor. – broot Nov 06 '21 at 20:24
  • @CharanSai Without looking at the implementation for `delay`, my guess is the continuation is removed from scheduling and, as broot mentions, a task is assigned to something like `ScheduledExecutorService`. When that task is complete the continuation is put back in the queue for eventual execution. – Slaw Nov 06 '21 at 20:24
  • When a coroutine calls a non-blocking, async operation it is said to be suspended. Does the application still handle that async operation in a separate thread? – Charan Sai Nov 06 '21 at 20:24
  • It is not that easy to answer what happens when IO operation is invoked, because coroutines themselves do not provide any support for IO. So it really depends on IO library. If the IO library expose suspendable API, but internally it uses blocking IO, then most probably it starts additional threads for IO. It can use non-blocking IO and selectors to at least handle multiple IO operations (so multiple suspended coroutines) with smaller number of IO threads. Or it can use event-based IO and then I believe it doesn't need any IO threads. Sorry, I'm not C++/OS expert to know this for sure ;-) – broot Nov 06 '21 at 20:31
  • Coroutines only provide `Dispatchers.IO` which is just a dedicated pool of threads for performing blocking operations. But as I said: IO is only one possible reason to suspend. Any kind of synchronization between coroutines also require frequent suspending and then I guess we don't need any additional threads for this. It works something like this: coroutine A schedules resuming of coroutine B using executor C. Then executor does the rest. – broot Nov 06 '21 at 20:36
  • okay. one more thing do we say coroutine is suspended when it calls a suspend function or the async operation in a suspend function? like at what point is the thread set free? – Charan Sai Nov 06 '21 at 20:42
  • The latter: invoking a suspend function itself does not suspend the coroutine. By invoking a suspend function we are informed that at some point in time it may (but doesn't have to) suspend. We don't know if and when it happens, so we always should assume it may suspend and that the thread may change between before and after invoking this function. – broot Nov 06 '21 at 21:05
0

import kotlinx.coroutines.*
import java.util.concurrent.Executors

suspend fun main(args: Array<String>) {
    coroutineScope {
        val start = System.currentTimeMillis()
        launch(Executors.newSingleThreadExecutor().asCoroutineDispatcher()) { // #1 
            launch {
                println("1 --> : " + (System.currentTimeMillis() - start))
                Thread.sleep(1000L)
                println("1 <--: " + (System.currentTimeMillis() - start))
            }

            launch { // #3
                println("2 --> : " + (System.currentTimeMillis() - start))
                delay(1000L)
                println("2 <--: " + (System.currentTimeMillis() - start))
            }
        }
    }

    coroutineScope {
        val start = System.currentTimeMillis()

        launch(Executors.newScheduledThreadPool(8).asCoroutineDispatcher()) { // #2
            launch {
                println("1` --> : " + (System.currentTimeMillis() - start))
                Thread.sleep(1000L)
                println("1` <--: " + (System.currentTimeMillis() - start))
            }

            launch { // #4
                println("2` --> : " + (System.currentTimeMillis() - start))
                delay(1000L)
                println("2` <--: " + (System.currentTimeMillis() - start))
            }
        }
    }
}

sample output:

1 --> : 56
1 <--: 1063
2 --> : 1063
2 <--: 2086
1` --> : 7
2` --> : 8
1` <--: 1012
2` <--: 1012

watch out for #1 and #2 lines, in #1 we launch coroutine with a single thread executor (something similar to Dispatchers.Main), so the coroutine are back-ended with a single thread, when we call a blocking function Thread.sleep, there's no change to start coroutine in #3, so #3 gets invoked after the entire launch,

as for #2, the same thing happen, but there are lots of remaining thread to let #4 gets launched

so

But in our kotlin case who exactly is executing those methods? Shouldn't there be a thread which manages stuff? So do we start a new thread to take care of the async code in coroutines?

not a thread, but a CoroutineContext, e.s.p CoroutineIntercepter which may defined in the parameter of CoroutineBuilder(lets say ,launch or something) or the parent scope,

all stuffs are not os/platform specific but user-land scheduler controlled by programmer

Minami
  • 963
  • 6
  • 21