```fun main() = runBlocking { val job = launch...
# coroutines
i
Copy code
fun main() = runBlocking {
    val job = launch {
        println("Coroutine start")
        launch {
            println("Child coroutine start")        
            println("Child coroutine end")

            launch {

                println("Child-inner coroutine start")        
                println("Child-inner coroutine end")
            }
        }
        println("Coroutine end")
        println("Coroutine end!")
    }
    println("Join")
    println("Done")
}
Why is Join and Done executed before job? And when calling job.join() between println("Join") and println("Done") job is executed at the place of calling job.join()?
d
runBlocking
, is an event loop. It executes tasks one by one. The first task is the lambda you pass to
runBlocking
, it is executed completely ("Join" and "Done" happen). Then it starts to execute the other tasks in the queue one by one (job is the first one). If you .join on the job, the lambda passed to
runBlocking
is split into two tasks: first part until .join and 2nd part after. In between it is suspended and the event loop executes other tasks
👍 2
o
https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md#coroutines-overview it does not have to be an event loop (many dispatchers use a thread pool), scopes are not queues (that is strictly part of the Dispatcher's internal implementation), but rather a way to inherit properties from the parent jobs (structured concurrency)
l
in a thread pool you submit tasks that executed, in an event loop you submit tasks that get executed. conceptually they are similar. usually in an event loop you should never block. in a thread pool you can block a thread and it is fine. looking at coroutine code to me it looks like it uses a thread pool to queue tasks which are made of coroutine sections. meaning it takes a suspend function and splits it into section at suspension points and those sections are queued in thread pools.
o
whether it is a thread pool or event queue is dependent on the dispatcher: Default/IO are pools, Main is usually an event loop,
runBlocking
is an event loop
Dispatchers.UNDISPATCHED is a little special, I don't think it is either a pool or a loop (runs on the thread that resumes, whatever it is)
d
Coroutines can be broken down as follows: 1. Suspension points are syntactic sugar for using callbacks. 2. Context provides a way to control the executor/scheduling and an inheritance structure among coroutines. Here, a coroutine is a sequence (imperative, not concurrent) of tasks. Only when you introduce more than 1 coroutine, is when it becomes concurrent.
l
🤔 🤔 that's interesting. Can you elaborate on what you mean by
Suspension points are syntactic sugar for using callbacks.
?
u
Imagine it how it actually is = nested callbacks, i.e. sequences of "and then.." actions, just the level of indentation if sugared away
b
Actually under the hood it works a bit different. It's state machine where suspension points are states. You can watch Deep dive into Coroutines by Roman Elizarov for more details.
e
Re-posting, fixing the attachments… Here’s the talk that @bezrukov mentioned:

https://www.youtube.com/watch?v=YrrUCSi72E8

There’s also a really good one from the GOTO conference where Roman gives a visual representation of how coroutines work and compares them to other ways of doing async:

https://youtu.be/hQrFfwT1IMo

If you’re interested, here’s the way I visualize coroutines: 1. Dispatchers - schedulers which take coroutines off a queue and run them on backing thread pools. Examples are
Dispatchers.Default
and
<http://Dispatchers.IO|Dispatchers.IO>
2. Coroutine context - Collection of elements which make up a coroutine, such as a
Job
which handles cancellation and the
Dispatcher
which schedules coroutines 3. Coroutine scope - a logical block which launches sibling coroutines and cancels others if a failure occurs. Launching within this scope queues jobs to the dispatcher, which will eventually run them on the backing thread pool 4. Suspending function - a function which can immediately exit (suspend) and give control back to a dispatcher’s worker thread when another suspending function is invoked. What really happens when you invoke another suspending function is you queue the next suspending function call on the dispatcher, immediately exit the function, then when the other suspend function is done you pick back up where you left off. Where suspending functions really shine is the fact that you can use
withContext
to launch a job on a separate thread pool and your current thread pool won’t be blocked by the other job. And yes, it is safe to think of the Dispatcher as an event loop because it kind of is one.
d
I was trying to simplify 😅
I think the materials linked here can clarify how they act like callbacks
l
Thanks, this is really helpful stuff! I'm definitely going to sit down and watch those two videos.