I have an itch that I cannot scratch, because I haven’t found a satisfactory answer online.
How does a coroutine know when to resume the coroutine state machine?
For context:
Let’s say I am using Ktor server and I need to make 2 Http calls to my downstream services.
My Ktor Http client returns an object I can await for each of those.
Since, coroutines allow us to reuse threads (even on the IO pool), I imagine both http calls can be made concurrently using the same thread on the pool. Now, when the http call comes back from the downstream service. What tells the coroutines to resume and read context from the state machine?
I can give some input from my understanding for references:
suspend function modifier that make compiler add Continuation<*> param at last parameter of function
That continuation instance using for store state and passing to child suspend function to callback when result available. (I think that is what trigger resume in your question)
Reuse threads mean suspend function does not block thread to waiting future result, whenever it get COROUTINE_SUSPENDED it will return COROUTINE_SUSPENDED and release callstack and let thread working on next task on queue.
Coroutine doesn’t know that. Whoever asked the coroutine to suspend, is responsible to resume it at a later time.
Taking delay/sleep function as an example: the function asks the coroutine machinery to suspend the current coroutine. Coroutine gets suspended and the function receives a continuation. Continuation is an object that “knows” how to resume the coroutine. Delay function has to store the continuation somewhere and set some kind of a timer. After the timer, it uses the continuation to resume the coroutine. Coroutine is scheduled to be executed according to its dispatcher.
(actually, the real “delay” function may work a little differently as the coroutines framework supports delayed execution natively)
It is the same with I/O. Current coroutine is suspended and whenever the I/O operation completes, the code that internally waited for I/O resumes the coroutine. But it really depends on the specific implementation of I/O. If it uses blocking I/O underneath, then there is no way to wait for it without blocking threads. If we do 2 concurrent operations, both of them suspend the caller coroutine (so do not block the caller thread), but they block 2 internal “I/O threads”. If we use non-blocking I/O underneath, then we either do not block any threads (we receive I/O events) or we block a single one for multiple operations (we select/poll from multiple resources).