I'm trying to understand the basics of Coroutines,...
# coroutines
c
I'm trying to understand the basics of Coroutines, the basics, but I don't understand at all what "suspending" means in this coroutines topic. Let's suppose I have my suspending function
fun suspend foobar() { dostuff }
, if I call that function from a coroutine, I understand from the documentation that calling foobar will suspend the coroutine, so does that mean foobar won't be executed? Why would I want to do that? It does not make any sense in my head. I know I must be understanding something really wrong
z
If a function is marked as
suspend
, that doesn’t mean it will suspend the coroutine, it just means that it might.
c
When will it suspend the coroutine exactly?
z
I find it helpful to always map coroutine concepts back to callbacks in my head, because that’s what they are under the hood. In your example, a callback rewrite might look like:
Copy code
fun foobar(continue: () -> Unit) {
  dostuff
  continue()
}
in this case, the function doesn’t “suspend” because it synchronously invokes the callback itself.
👍 1
If this function were to “suspend”, it wouldn’t invoke the callback, but instead somehow schedule the callback to be invoked later, or pass it to another API that delivers a result through a callback. E.g.
Copy code
fun foobar(continue: () -> Unit) {
  executor.execute {
    dostuff
    continue()
  }
}
👍 1
c
Aren't all "normal" function "suspend" functions? Because, when they are called, they "block"/"suspend" the routine which called it until they return their result
What's the difference then?
c
The simplest explanation is that a function marked
suspend
is chopped up into smaller pieces, and those pieces do not need to be processed immediately one after the other. Instead, each block can be suspended to be resumed as some later point in the future, potentially on a different thread. The compiler does the work of chopping a
suspend
function into these chunks, and the runtime library does the work of suspending/resuming these chunks.
z
that’s a great question! You’re right, the caller temporarily stops execution while the child runs. The difference is that, with normal functions, the thread that the caller is on stays occupied running the child, and the thread can’t do anything else until the child returns. With suspend functions, the rest of the execution of the function can get wrapped up into a callback and later invoked somewhere/sometime else, and the thread can be freed up.
c
This article goes pretty deep into this topic, but is very helpful for understanding the difference. At the surface, the concept of blocking vs suspending seems very similar, but it’s in the way the code is actually executed at runtime that makes them very different, and where the power and flexibility of coroutines comes from https://medium.com/@elizarov/blocking-threads-suspending-coroutines-d33e11bf4761
1
c
Thank you! I hope I have more luck this time, I have already watched two hours of video of conferences from Roman Elizarov 😂
I still don't understand the basic idea of "suspending"
It sounds a bit like magic
How does the compiler know where to make the "chunks"?
When a network call is made for example? How does it identify a network call and that it has to wait?
c
The basic idea is that it’s chopped at any point where another
suspend
function is called, which is called a “suspension point”. This is why
suspend
functions can only be called within other
suspend
functions/lambdas. By being inside a
suspend
function, the compiler knows to start chopping things up when calling other
suspend
functions
There’s nothing specific about “suspending at a network call”. It’s simply that the networking library has
suspend
functions inside it
c
That sounds like it's going to "chop" a lot more than necessary, specially when you have a stack trace of lots of suspend function calls that are simply "suspend" because there's some needed really async/suspend call at the bottom of the stack
And you can't call them outside of suspend functions
And suspending sounds a lot like sequential function calling, where calling means blocking the execution 🤔 What's new here?
What's the point of having a concurrency library when every core idea is built orbiting the idea of blocking an execution? I'm missing something really hard
c
Yes, that’s all true. There are a lot of chunks being made, the bytecode generated is quite a bit larger, and the execution is sequential by nature. By unlike blocking a thread, a suspending coroutine consumes virtually no system resources. Blocking a thread means that thread cannot process anything else. Suspending a coroutine means that the thread executing that chunk is free to handle other chunks that are ready to continue working. While a powerful compuer can only have a few thousand threads active, you can easily have millions of coroutines active at one time, even on a single-threaded program (such as in Kotlin/JS)
The core idea is that coroutines are not dependent upon threads (either OS threads or virtual threads). It can use multiple threads to great effect, but multiple threads are not necessary. It is concurrency that works even on a single thread.
That said, the main reason to use coroutines is in being able to naturally express concurrent or async code sequentially, which cannot be done in normal blocking code. You’d have to use callbacks to manage concurrency without coroutines, and callbacks by nature are messy to use and become deeply nested in deeply concurrent code.
c
That makes me wonder why it wasn't implemented in any other "popular" language, what's the disadvantage?
I can only think of, that if a coroutine provokes an error, the whole thread (and it's 100K coroutines inside) could die
z
How does the compiler know where to make the “chunks”?
I think it’s probably more accurate to say this happens at the special
suspendCoroutine
function. This is the function that actually gives you a callback that represents the rest of the function.
It has been, both C# and JavaScript have async/await, which is a similar thing (but less flexible)
Swift is getting it too i think
And Go is probably the modern language most famous for using coroutines (called “goroutines” to be cute)
Ruby has Fibers which are similar, Python has generators which use a similar mechanism (and also async/await now maybe?)
c
It sounds really complex to me, so If you have 2 threads and 500 coroutines waiting to resume execution, you will need a policy to decide what coroutine is resumed, that decision could fail badly, it's like doing the work of the OS but made by the Kotlin compiler 😂
I'm very confused right now
And all those chunks being called, all those additional jumps in the CPU, how can this work so efficiently?
Sorry guys, I want to understand it before using it
c
you will need a policy to decide what coroutine is resumed
Again, yes.
Dispatchers
are that policy. But even Java’s exceptions are a layer of error-handling built above the OS level. Most languages and frameworks need to implement their own form of error-handling specific to that domain, and this, too, is part of coroutines. Under-the-hood, it’s basically just throwing and catching exceptions with a small bit of logic to track which chunks threw the exception and which ones caught it (basically, the
Job
interface).
how can this work so efficiently
If you’re constantly jumping between threads, it’s just as expensive as a normal thread context-switch. But most code don’t jump between threads that often, just a couple of times, and when it’s “jumping” between chunks without that thread switch it’s little more than a regular function call
Also, in terms of efficiency, a small number of threads can be used to process large numbers of these “chunks”. You don’t need to create new threads for each one, you can reuse existing threads, which greatly improves the efficiency
Maybe thinking of the thread as a conveyor belt will help. Normal blocking code is the the conveyor. Coroutines just happen to use a conveyor to move boxes around, but the conveyor belt itself isn’t so important. It could be a slide, or a cannon, or team of tiny gnomes moving those boxes around for all coroutines cares.
c
As Roman explains in the article you gave me, there is two natures of blocking, an extremely cpu intensive task, and waiting for a looong I/O operation, so coroutines only "solve" the problem for waiting at a long I/O operation (where the cpu is idle, only waiting), isn't it? It might sound a kinda dumb question but I'd like to be sure
e
no, coroutines are useful for other purposes as well
in Kotlin 1.4, there's an experimental
DeepRecursiveFunction
that allows you to write a deeply recursive function as a suspend function. with how the compiler automatically chunks up suspend functions, this converts stack usage into heap usage
c
While CPU-bound tasks are more difficult to manage, they do still fit into the overall architecture. You just need to know that code can still block a thread in coroutines, and that they don’t magically turn blocking code into non-blocking code. If a CPU core is stuck running or forcibly waiting idle (
Thread.sleep()
), then coroutines can’t do too much. Ultimately, it’s just running normal code in smaller chunks interwoven in clever ways, but they can’t really do anything in the middle of a chunk. CPU-bound tasks are essentially just really large chunks, and as such the framework can’t fix that for you. But they can make it easier to move that giant chunk to the threads best suited for processing it (
Dispatchers. Default
), or providing you tools for breaking a large blocking code chunk into smaller chunks (
suspendCoroutine
Zach mentioned above)
c
Thank you very much to all, I have to think but it's clearer than an hour ago
🎉 3
c
If it helps... the whole "suspend" thing has confused me since day 1 too. I just think of them as syntactic sugar for callbacks and call it a day.
☝️ 2
a
Super interesting thread. Took me a while to understand not only
suspending
but also what
structured concurrency
fully meant in the context of coroutines
i
@Carrascado This might help: https://github.com/JetBrains/kotlin/compare/document-coroutines-codegen I tried to cover everything, but there are still ~2k lines to go (fix grammar and style etc.). You can skip parts unrelated to your question, such as "Suspend Marker", "Layout" etc (it can help to understand how exactly the compiler does its job, but it might be too specific) and focus only on suspension, resumption and interception.
😮 1
c
Thank you guys for your answers! At least I don't feel alone anymore 😂 Thank you! @Ilmir Usmanov [JB]! I'm gonna check it out
c
Here’s another article that actually inspects the (decompiled) bytecode generated by the compiler, which helped me understand more about how the compiler “chops up” the code and wires it all together with Continuations https://www.infoq.com/articles/kotlin-coroutines-bottom-up/
c
Thank you @Casey Brooks I'm gonna check it out too, I hope I understand them better that way!