Reading Coroutine official guide thoroughly — Part 5— Dive 2

What is the relationship among Coroutine, Suspending function and Job?

Myungpyo Shim
6 min readAug 18, 2021

[Korean] English

(Return to the table of contents)

Let’s deep dive into coroutine world and figure out the relationship among its internal elements. After reading this, you can explains the role of each coroutine component and the relationship among them.
(Those coroutine components are marked by 📌 )

This posting uses examples from the previous posting (Dive1)

Let’s take a look at the ViewModel in the example.

At first, SampleViewModel class implements 📌CoroutineScope interface. This means that this view model class is coroutine scope itself and all of the coroutines created and launched in it use this scope as their default context.

CoroutineScope interface declares 📌CoroutineContext internally. For that reason, the view model which implements this coroutine scope must implement this interface. In the example, coroutineContext is implemented as combining main thread dispatcher (MockDispatchers.MAIN) and SupervisorJob and all of the coroutines launched in this scope will be executed base on this coroutine scope.
(See this Dive to catch up with the basics of coroutine context and elements.)

In the example, when you call syncUserSetting() function in the ViewModel, a coroutine to execute the suspending lambda { block } will be created. At this point, the newly created coroutine creates its own coroutine context based on the context of coroutine scope.

You can see the declaration of the launch coroutine builder below.

Each parameter means,

  • context : Basically newly created coroutine uses parent’s coroutine context (in the coroutine scope) as its base context. when you pass a particular context to this parameter, the coroutine overrides the base context.
  • start : Denotes how the coroutine is executed(e.g. start immediately, start lazily, start as non-cancellable one, etc).
  • block : To be executed 📌suspending function code block in the CoroutineScope.

And the return type of this coroutine builder is Job.

The last parameter of the launch coroutine builder is block and the block will be the starting function of the coroutine. Then what is the coroutine exactly? Isn’t it still unclear?

Let’s take a look at the declaration of coroutine. The most of coroutines are implementation of AbstractCoroutine. So let’s see that first.

Focus on the last part. 📌 Job, 📌 Continuation, 📌 CoroutineScope.
A coroutine itself is Job, Continuation and CoroutineScope at the same time as implementing those interfaces.

A coroutine created in a coroutine scope and it inherits context elements from the context of the scope. The context of the scope is parentContext in the above code snippet.

In addition, a coroutine itself is a coroutine scope. As I mentioned earlier, a coroutine scope must declare coroutine context. The AbstractCoroutine class creates Job context element and combines parent context and the newly create job context in order to use the combined context as its context of scope.
coroutine context = parent context + this (job)

Job is state-machine of a coroutine and the implementation detail is not in the AbstractCoroutine class but in the JobSupport class. All the coroutine jobs make job hierarchy and each job in the hierarchy can propagates its event such as cancellation or error to upward or downward through ParentHandle and ChildHandle.

When we execute below code,

launch { ... }

The coroutine will take below steps internally.

- Create parent coroutine context 
: parent context + additional context elements + Debug ID
- Start newly created coroutine
- Start Parent's job if it is necessary
- Bind current (child) job to the parent job through Handle
- Start suspending function of the coroutine

Below code snippet is to start the suspending function which is the last step of coroutine creation.

The process of creating and starting a coroutine is like below.

createCoroutineUnintercepted -> intercepted -> resumeCancellable

Let’s see that step by step.

* createCoroutineUnintercepted( )

When this function is called, there will be state-machine created in order to execute the suspending lambda block.
(Keep in mind that below code is simplified version)

At the first glance, it looks so complicated but no worries 😅. You can separate it as two main part: resumeWith and invokeSuspend.

resumeWith( )
A suspending function references the context of the coroutine. When resumeWith() function gets called after its creation, it runs its internal logic. At this time, if there are other nested suspending functions in it (ex> delay(), yield(), …), invokeSuspend() function returns COROUTINE_SUSPENDED which is reserved constant and the coroutine waits until resumeWith() function is called again. If invokeSuspend() function returns value, it keeps call invokeSuspend() function until it meets the last suspending point. When the last suspending point is passed, the state-machine notify the last result to the caller through the completion which is passed as a parameter.

completion.resumeWith(result)

invokeSuspend( )

invokeSuspend() function is state-machine that declares all the suspending point of the suspending function. Default label is 0 and the last label number depends on the number of suspending points. For instance, in the previous user setting synchronization example, the suspending block which is passed to the launch { } coroutine builder only calls 1 suspending function (Repository.syncUserSetting()). So, all of the labels defined are 0 and 1. However syncUserSetting() suspending function in the repository has 3 suspending point like below. For the reason, it has 4 label: 0, 1, 2 and 3.

Let me depicts the process as using my humble picture.

The coroutine on the left side is created in the ViewModelScope and the coroutine context of the scope is composed of Dispatcher = Main and Job = SupervisorJob; after that it launches child coroutine through withContext(Dispatcher.IO) { } coroutine builder. At this point, the child coroutine redeclares its dispatcher as IO Dispatcher so that it can be executed on a thread from I/O thread pool and it has a new job since every coroutine has a newly created job when it is created.

When syncUserSetting() suspending function is called, a state-machine which is composed of suspending functions to sync user settings is created and started by the coroutine framework, and the final result of the state-machine is being returned to the caller.

In the above picture, each suspending point (Suspend task #n) is also a Continuation but I omit it for brevity.

📌 Continuation is resuming point after its suspension as its name explains. In addition, most of the implementations of the continuation also implement CoroutineStackFrame. Due to the fact that it implements CoroutineStackFrame, it can keep the suspending function call chain and generate stack traces when it gets interrupted by unexpected exception.

This is similar to the stack-unwinding of functions like below.
Call : Func A -> Func B -> Func C
Return Func C -> Func B -> Func A

Coroutine has a special nick name, Light-weight thread. In fact, coroutine is just executable code block but the nick name is not that wrong since it offers its own call stack like above.😁

* intercepted( )

You can see that intercepted() function is called right after createCoroutineUnintercepted() function call. You need to know what the interceptor is in the coroutine to understand this intercepted() function. All the types of dispatchers we have used until now (ex> Main, I/O, etc) implement ContinuationInterceptor interface. ContinuationInterceptor is one of the elements of coroutine context.

In other words, if you specify Dispatchers.IO as a dispatcher element when you create coroutine, the dispatcher is registered as the type of ContinuationInterceptor to the map of context elements.

Then what is the role of the function intercepted() ?
When intercepted() function is called to a Continuation<T>, the Continuation is transformed to the DispatchedContinuation.
When a coroutine is resumed after suspended state and the Continuation is a type of DispatchedContinuation, the continuation is dispatched to the applicable thread if the dispatcher of current coroutine context is differ from continuation’s. If you execute a coroutine which is not intercepted, the coroutine is executed right away.

* resumeCancellable( )

The final step of starting a coroutine is calling resumeCancellable() function. This function calls the starting suspending function of the coroutine so that the state-machine of the coroutine become up and running. At the very first time of the state-machine, the result is Unit because there is no result value yet.

We’ve covered the elements of the coroutine and the relationship among them until now. In fact, the state-machine is just labeled code stub which is generated from coroutines or suspend functions by kotlin compiler. We can program asynchronous logic sequentially and intuitively as using suspend keyword thanks to the coroutine framework.

The end.

--

--

No responses yet