Kotlin Coroutine mechanisms part 3: swapping CoroutineContext
Part 3 — CoroutineContext, Dispatchers, runContext, and Android viewModelScope explained
This series serves as spin off from Programming Android with Kotlin: Achieving Structured Concurrency with Coroutines intended to help strengthen everyday coroutine understanding through playful explorations. We [the authors] have always had sincere intentions writing the book:
While [coroutine] concepts are important if you want to master coroutines, you don’t have to understand everything right now to get started and be productive with coroutines. — Chapter 9: Coroutine concepts p. 127
References to the book may be denoted as Programming Android with Kotlin for brevity. Navigate around for previous articles in this series:
- Kotlin Coroutine Mechanisms part 1: runBlocking v. launch
- Kotlin Coroutine Mechanisms part 2: launch v. async
- Kotlin Coroutine Mechanisms part 3: swapping CoroutineContext
To follow along for the Android portion of explorations, download this starter Android project.
Last we left off
So far, we’ve explored how runBlocking
, launch
, and async
behaviors interact with one another. Our latest code block looked something like this:
fun main() = runBlocking {
val job = launch(Dispatchers.Default) {
...
val task1: Job = launch {
/** Fire-and-forget heavy work **/
}
val task2: Deferred<String> = async {
/** Do heavy work and wait for return **/
}
// wait for coroutine to complete before moving on
task1.join()
val task3 = launch {
/** Fire-and-forget heavy work **/
}
...
// return answer to task2 here
println(task2.await())
...
}
Perhaps you’ve noticed how job
and task
invokes the launch
call two ways, with a designated CoroutineContext
and without. In pseudocode:
job = launch(context) {
...
task = launch { ...}
...
}
What does this do? What is a coroutine context? Why does it matter when using Kotlin coroutines? This article intends to answer these questions via Android.
What is CoroutineContext
?
CoroutineContext
, or for brevity, context, represents persistent context for a coroutine. In the case of a coroutine, context designates what task executes where. Managing context is important - switching what task executes on what thread or thread pool gives capacity to tweak concurrency behavior.
Context be used from the Dispatchers.kt class. In our Android project, CoroutineContextDispatcher
is written to allow for dependency injection:
JVM Kotlin documentation and Dispatcher base platform class describes nuances between each of these context elements:
Dispatchers.Main
[The Main] coroutine dispatcher that is confined to the Main thread operating with UI objects. Usually such dispatchers are single-threaded — Dispatchers.common.kt.
On the JVM, using Dispatchers.Main
requires another dependency offered in Android, JavaFX, and Swing. On JS, it is equivalent to Dispatchers.Default
. For Native targets, Dispatchers.Main
is not available unless using a Darwin-based target.
Dispatchers.Default
Dispatchers.Default
is the default context argument for standard coroutine builders. The maximum size of parallel threads that can be used for its assigned task is equal to the number of processors available on the CPU. Dispatchers.Default
is best for CPU-intensive tasks.
[Dispatchers.Default] is backed by a shared pool of threads on JVM and Native. By default, the maximum number of threads used by this dispatcher is equal to the number of CPU cores, but is at least two. — Dispatchers.common.kt :
Dispatchers.IO
Dispatchers.IO
indicates an elastic thread pool that defaults to 64 threads or cores, whichever number is larger. It’s perfect for IO tasks, since threads in this pool is expected to spend most of the time in a non-running state.
[Dispatchers.IO] and its views share threads with the
Dispatchers.Default
dispatcher, so usingwithContext(Dispatchers.IO) { … }
when already running on theDispatchers.Default
dispatcher typically does not lead to an actual switching to another thread. — JVM Dispatchers.kt:
Dispatchers.Unconfined
Dispatchers.Unconfined
is not defined in the class above; it is intended for internal use and should not be used in production code. As the naming suggests, it is not confined to any thread. Kotlin documentation provides additional detail on the nuances of behavior of this dispatcher.
This dispatcher is best used in unit testing. In the case of our Android project, we can override the CoroutineContextProvider
interface and assign all values to Dispatcher.Unconfined
. We won’t cover unit testing today, but hey, I’ll get to it sometime if enough requests come in.
How can CoroutineContext be used?
As mentioned in the beginning of the article, context in coroutines can be used in different ways:
- Within Coroutine builder extension functions like
launch(context)
CoroutineScope(context)
— context defined within aCoroutineScope
withContext(context)
— as a way to swap context within a specific coroutine
Within a Coroutine builder extension functions i.e. `launch(context)`
All standard coroutine builders can be launched with a designated coroutine context. In the implementation for launch
, its first argument context
has default value EmptyCoroutineContext
:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
...
block: suspend CoroutineScope.() -> Unit
): Job { ... }
For this reason, you can invoke builders without a context argument:
runBlocking {...}
launch {...}
async {...}
withContext {...}
On startup, our application calls loadContent
to do some heavy work and render the screen after.
doHeavyWork
is a suspending function denoted by the keyword suspend
, and operates on the the current context of the task it was called from. Within function execution, a suspending function may pause at suspending points to be resumed at a later time.
When doHeavyWork
starts, child coroutines launches in the forEach
statement. These child coroutines are set to Dispatchers.IO
via launch
argument to complete some IO work.
When doHeavyWork
completes and result
returns, execution shifts back to the original dispatcher with the result
. That result can then update UI via observable viewState
. In the next section, we look closer at setting context within scope.
CoroutineContext defined within `CoroutineScope(context)`
Historically, I’ve found explanations of context and scope in other frameworks to be a bit circular. Luckily, Programming Android with Kotlin succinctly describes CoroutineScope
:
A
CoroutineScope
controls the lifecycle of a coroutine within a well-defined scope or lifecycle. Its purpose is to manage and monitor the coroutines you create inside of it . — Chapter 9: Coroutine Concepts, p. 128
CoroutineScope
holds a reference to the CoroutineContext
so that it encapsulates other coroutine tasks within.
Take the case of the MainActivityViewModel
, which makes use of LiveData
to reflect back in Composable UI. MainActivityViewModel
makes use of both viewModelScope
and a custom scope
.
By default, viewModelScope
launches coroutines on Dispatchers.Main
. When loadContent
is called, viewModelScope
launches an Activity lifecycle-aware coroutine. Within, scope.launch
then calls on doHeavyWork
to start.
When we run the application, it immediately crashes. Can you guess why?
FATAL EXCEPTION: DefaultDispatcher-worker-1
Process: com.example.kotlincoroutinemechanisms, PID: 8121
java.lang.IllegalStateException: Cannot invoke setValue on a background thread
at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:502)
at androidx.lifecycle.LiveData.setValue(LiveData.java:306)
at androidx.lifecycle.MutableLiveData.setValue(MutableLiveData.java:50)
at com.example.kotlincoroutinemechanisms.MainActivityViewModel$loadContent$1$1.invokeSuspend(MainActivityViewModel.kt:35)
...
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@522a2d9, Dispatchers.Default]
Because scope
is set to Dispatchers.Default
, doHeavyWork
runs on a background thread, but the work does not return to the Main thread context launched by viewModelScope
. When doHeavyWork
completes, its result returns and execution shifts back to the original dispatcher withresult
.
We can then update observable viewState
with result
once retrieved. However, the code attempts to update _viewState.value
with result
while still in the background thread. To fix this issue,withContext
comes to the rescue.
CoroutineContext defined within `withContext(context)`
When working with background execution on viewModelScope
or a coroutine task, you can use withContext(context)
to switch the current task execution to a more appropriate dispatcher.
However, the code attempts to update _viewState.value
with result
while still in the background thread.
When the result returns from background thread work in doHeavyWork
, we can then switch the context at arrival of our Result type using withContext(contextPool.mainDispatcher)
:
When we run the application again, the application does not crash. In logging, we get:
suspend doHeavyWork() | current thread: DefaultDispatcher-worker-1
task1 | count: 0 | current thread: DefaultDispatcher-worker-2
task1 | count: 1 | current thread: DefaultDispatcher-worker-2
task1 | count: 2 | current thread: DefaultDispatcher-worker-2
task1 | count: 3 | current thread: DefaultDispatcher-worker-2
task1 | count: 4 | current thread: DefaultDispatcher-worker-2
loadContent succeeded: Success(data=5) | current thread: main
Lesson Recap:
CoroutineContext
designates what task executes on which thread or thread pool. We can use it to tweak the behavior of concurrent flow.- There are 4 dispatchers that can be used:
Dispatchers.Default
,Dispatchers.Main
,Dispatchers.IO
,Dispatchers.Unconfined
CoroutineContext
can be used in 3 different ways:launch(context)
,CoroutineScope(context)
andwithContext(context)
Want more content like this?
That’s all for now for this series. Keep an eye out for news of online workshop content coming soon — hit subscribe to get the latest updates on more content!
For more in-depth content related to this material, consider looking into Programming Android with Kotlin: Achieving Structured Concurrency with Coroutines — namely, Chapter 9: Coroutines concepts.