Asynchronous Programming in Kotlin
Asynchronous Programming in Kotlin
Asynchronous
Programming
in Kotlin
● Kotlin coroutines
● Inside CoroutineScope
● Channels
● More
Parallel programming
blocking
call
Blocked
Parallel programming
● In reality, programs (threads) spend a lot of time waiting for data to be fetched from disk,
network, etc.
● The number of threads that can be launched is limited by the underlying operating system (each
takes some number of MBs).
● Threads aren’t cheap, as they require context switches which are costly.
● Threads aren’t always available. Some platforms, such as JavaScript, do not even support them.
● Working with threads is hard. Bugs in threads (which are extremely difficult to debug), race
conditions, and deadlocks are common problems we suffer from in multi-threaded
programming.
Thread
An example
What we want
submitPost
Thread
processPost processPost
An example
What we get
Thread
processPost processPost
An example
With callbacks, the idea is to pass one function as a parameter to another function and have this one
invoked once the process has completed.
● There are different APIs, which vary across libraries, frameworks, and platforms.
This looks and feels sequential, allowing you to focus on the logic of your code.
marks suspension points in IntelliJ IDEA.
The history of coroutines
History and definition
● Melvin Conway coined the term “coroutine” in 1958 for his assembly program.
● Coroutines were first introduced as a language feature in Simula’67 with the detach and
resume commands.
● A coroutine can be thought of as an instance of a suspendable computation, i.e. one that can
suspend at some point and later resume execution, possibly even on another thread.
● Coroutines calling each other (and passing data back and forth) can form the machinery for
cooperative multitasking.
Coroutines came to Kotlin in version 1.1, and they became stable in version 1.3.
Into:
fun submitPost(token: Token, item: Item, cont: Continuation<Post>) {...}
Where:
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
processPost
token submitPost
preparePost post
SUSPENDED
Practice
Now we can finally post items without blocking the execution thread!
fun nonBlockingItemPosting(...) {
...
postItem(item)
}
Practice
Now we can finally post items without blocking the execution thread!
fun nonBlockingItemPosting(...) {
...
postItem(item)
}
The suspending function postItem should be called only from a coroutine or another suspending
function.
jobs.forEach { it.start() }
● A child’s failure immediately cancels its parent along with all its other children. This behavior can be customized using
SupervisorJob.
Job States
wait for
children
cancel/fail
finish
Cancelling Cancelled
Job states
● Dispatchers.IO – A shared pool of on-demand created threads and is designed for offloading
IO-intensive blocking operations (such as file/socket IO).
Dispatchers
● A view of a dispatcher with the guarantee that no more than parallelism coroutines are
executed at the same time can be created via:
fun dispatch(
block: Runnable,
taskContext: TaskContext = NonBlockingContext,
tailDispatch: Boolean = false
) {
...
}
}
A peek under the hood
● Contexts can be added together. In this case, the rightmost value for a Key is taken as the
resulting context.
● Since each Element implements CoroutineContext, this looks like a sum of elements.
Context switching
Main
IO
Default
time
How is this actually better than threads?
Main post
IO
Default
time
How is this actually better than threads?
Main post
withContext(Dispatchers.IO)
IO fetch
Default
time
How is this actually better than threads?
Main post
IO fetch blocked
Default
time
How is this actually better than threads?
Main post
Default
time
How is this actually better than threads?
Main post
withContext(Dispatchers.Defalt)
Default process
time
How is this actually better than threads?
Default process
time
How is this actually better than threads?
Default process
time
How is this actually better than threads?
time
Coroutines - fibers - threads
WRONG!
The default behavior is sequential, you have to ask for concurrency.
Coroutines - fibers - threads
Exception in thread "main" java.lang.OutOfMemoryError: unable to create native thread: possibly out
of memory or process/resource limits reached.
Coroutines - fibers - threads
It is not guaranteed that the coroutine is going to be resumed on the same thread, so be very careful about calling suspending
function while holding any monitor.
Job
launch SupervisorJob
Job
launch SupervisorJob
Job
launch SupervisorJob
Job
launch SupervisorJob
Job
launch SupervisorJob
handler
Job
launch SupervisorJob
handler
Job
launch SupervisorJob
handler
Check this out
Job
Check this out
launch SupervisorJob
handler
Job Cancel
launch SupervisorJob
handler
Job
launch SupervisorJob
Cancel
handler
Job
launch SupervisorJob
handler
Job
launch SupervisorJob
handler
jobs.forEach { it.start() }
fun main() {
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
with(scope) {
val job1 = launch {
throw Exception("Some jobs just want to watch the world burn")
}
val job2 = launch {
delay(3000)
println("I've done something extremely useful")
}
}
scope.coroutineContext[Job]..let { job .>
runBlocking { job.children.forEach { it.join() } }
} ./ `job1.join()` will throw, so `it.join()` should actually be in a `try/catch` block
}
Error handling
fun main() {
./ `someScope: CoroutineScope` already exists
someScope.launch { ./ this coroutine is a child of someScope
supervisorScope { ./ SupervisorJob inside
val job1 = launch {
throw Exception("Some jobs just want to watch the world burn")
}
val job2 = launch {
println("Going to do something extremely useful")
delay(3000)
println("I've done something extremely useful")
}
}
}
...
}
Error handling
fun main() {
val scopeWithHandler = CoroutineScope(CoroutineExceptionHandler {
context, error .> println("root handler called")
})
scopeWithHandler.launch {
supervisorScope {
launch { throw Exception() }
launch(CoroutineExceptionHandler { context, error .>
println("personal handler called")
}) { throw Exception() }
}
}
...
}
Exceptions are not propagated to parents meaning you can override the handler.
Inside CoroutineScope
Structured concurrency
Error handling (revisited)
● GlobalScope – This is a delicate API and its use requires care. Make sure you fully read and understand the documentation of
any declaration that is marked as a delicate API. The delicate part is that no Job is attached to the GlobalScope, making
its use dangerous and inconvenient.
In the event of a downloadContent or processContent crash, the exception goes to the coroutineScope, which stores links
to all child coroutines and will cancel them. This is an example of structured concurrency, a concept that is not present in
threads.
A helpful convention
This function takes a long time and waits for something:
suspend fun work(...) { ... }
or:
fun CoroutineScope.moreWork(...): Job = launch { ... }
Not:
suspend fun CoroutineScope.dontDoThisPlease()
A helpful convention
● The coroutine (job) does not know that somebody is trying to cancel it.
● Cancellation is cooperative.
Cancelling coroutines
val job = launch(Dispatchers.Default) {
repeat(5) {
try {
println("job: I'm sleeping $it...")
delay(500)
} catch (e: CancellationException) {
println("job: I won't give up $it")
}
}
}
yield()
println("main: I'm tired of waiting!")
job.cancelAndJoin() ./ cancel + join
println("main: Now I can quit.")
Cancelling coroutines
Channel is like BlockingQueue, but with suspending calls instead of blocking ones.
● Channels are fair, meaning that send and receive calls are served in a first-in first-out order.
● By default, channels have RENDEZVOUS capacity: no buffer at all. This behavior can be tweaked:
The user can specify buffer capacity, what to do when buffer overflows, and what to do with
undelivered items.
Select (experimental!
Now that we know much more, let’s get a better approximation of what’s going on under the hood.
Under the hood
class PostItemStateMachine(
completion: Continuation<Any?>?,
context: CoroutineContext?
): ContinuationImpl(completion) {
var result: Result<Any?> = Result(null)
var label: Int = 0
We are given:
suspend fun suspendAnswer() = 42
suspend fun suspendSqr(x: Int) = x * x
fun main() {
.:suspendAnswer.startCoroutine(object : Continuation<Int> {
override val context: CoroutineContext
get() = CoroutineName("Empty Context Simulation")
...
cancellableCont.cancel(…)
}
Miscellaneous
async / await
async / await in Kotlin
async Task PostItem(Item item) {
Task<Token> tokenTask = PreparePost();
Post post = await SubmitPost(tokenTask.await(), item);
ProcessPost();
}
● await is a single function, but depending on its environment it can result in 2 different behaviours.
● The C# approach was a great inspiration for the Kotlin team when they were designing coroutines, as it was for Dart, TS,
JS, Python, Rust, C++...
async / await in Kotlin
fun CoroutineScope.preparePostAsync(): Deferred<Token> = async<Token> { ... }
Deferred<T> : Job is a Job that we can get some result from. async is just another coroutine builder. You can write exactly
the same in Kotlin!
Check out developer.android.com to learn how coroutines are used (extensively) in modern
Android development.
● All of the code from this presentation can be found in the corountines folder at
github.com/bochkarevko/kotlin-things/
Thanks!