Introduction
In this post, I’ll dive deep into multi-threading and concurrence in Kotlin programming language.
You can test the code snippets I’ll provide during the article in three ways:
- Google online editor: Kotlin Playground
- Official Kotlin Online Editor
- JetBrains’ IntelliJ IDEA (Ultimate or Community Edition)
Threads
A thread is an abstraction for how a processor appears to handle multiple tasks at once.
Managing threads takes up system resources and time. A running app will have multiple threads. Each app have at least the main thread (or UI thread), which is one dedicated thread responsible for the UI.
Please Note: In some cases, the UI thread and main thread may be different.
It’s important for the main thread to perform well, so that the app will run smoothly. Any long-running tasks will block it until completion and cause your app to be unresponsive.
Current phones attempt to update the UI from 60
to 120
times per second.
The time to prepare and draw the UI is very short: at 60
frames per second (fps
), screen updates should take at most 16ms
.
Some frames drop and fluctuation is normal, but too many drops will make an app unresponsive.
When multiple threads try to access the same value in memory at the same time, the OS will face the so called race condition. Performance issues, race conditions, and hardly reproducible bugs are some of the reasons why it’s discouraged to work with threads directly.
Main Concepts
Cooperative Multitasking
Coroutines enable multitasking. They have the ability to store their state, so that they can be stopped and resumed. A coroutine may also not execute.
In the cooperative multitasking flow, the state is represented by continuations, which allows portions of code to signal when they need to hand over control or wait for another coroutine to complete its work before resuming.
Creating a coroutine include working in a Job
, inside a CoroutineScope
, managed by a Dispatcher
:
Job
: cancellable unit of workCoroutineScope
: context that enforces cancellation (and other rules) to its children and sub-children recursivelyDispatcher
: determines the thread the coroutine will use
The launch()
function (invoked by a CoroutineScope
) creates a coroutine from the enclosed code wrapped in a cancelable Job object.
launch()
is used when a return value is not needed outside the the coroutine.
The full signature is:
fun CoroutineScope.launch() {
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
}
The block of code you pass to launch()
is marked with the suspended
keyword.
Suspend indicates that a block of code (or function) can be paused or resumed.
Lambda functions are suspended
: they can invoke other suspended
functions on their code block.
Similar to launch()
there’s the async()
function, which the following signature:
fun CoroutineScope.async() {
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
}: Deferred<T>
A Deferred
is similar to a promise or future and serves as a placeholder for a return value.