Kotlin Coroutines provide a new way to write asynchronous and concurrent code using suspending functions and structured concurrency. Coroutines avoid callbacks and allow sequencing of asynchronous code using async/await. Coroutines are lightweight threads that can be suspended and resumed. Channels allow coroutines to communicate and share data. Flows provide a reactive streams API for coroutines. Kotlin coroutines offer improvements over other asynchronous programming models like callbacks and RxJava.
Example: simple consecutivelogic
Use authentication service:
fun login(email: String): UserId
Send a request to a remote data base:
fun load(id: UserId): User
Show the results:
fun show(user: User)
14.
Example: simple consecutivelogic
fun login(email: String): UserId
fun load(id: UserId): User
fun show(user: User)
fun showUser(email: String) {
val id = login(email)
val user = load(id)
show(user)
} Simple, but wrong…
15.
Rewrite with CompletableFuture
funlogin(email: String): CompletableFuture<UserId>
fun load(id: UserId): CompletableFuture<User>
fun show(user: User)
fun showUser(email: String) {
login(email)
.thenCompose { load(it) }
.thenAccept { show(it) }
}
Looks complicated…
16.
Rewrite with RxJava
funlogin(email: String): Single<UserId>
fun load(id: UserId): Single<User>
fun show(user: User)
fun showUser(email: String) {
login(email)
.flatMap { load(it) }
.doOnSuccess { show(it) }
.subscribe()
}
Looks even more complicated…
17.
Using async/await inKotlin
fun login(email: String): Deferred<UserId>
fun load(id: UserId): Deferred<User>
fun show(user: User)
fun showUser(email: String) = async {
val id = login(email).await()
val user = load(id).await()
show(user)
}
runs the code asynchronously
awaits the result of the
asynchronous computation
18.
Using async/await inKotlin
fun login(email: String): Deferred<UserId>
fun load(id: UserId): Deferred<User>
fun show(user: User)
fun showUser(email: String) = async {
val id = login(email).await()
val user = load(id).await()
show(user)
}
Looks better…
19.
Rewrite with suspendfunctions
suspend fun login(email: String): UserId
suspend fun load(id: UserId): User
fun show(user: User)
suspend fun showUser(email: String) {
val id = login(email)
val user = load(id)
show(user)
}
Looks exactly like the initial code!
fun test(email: String){
showUser(email)
}
Error: Suspend function 'showUser'
should be called only from a coroutine
or another suspend function
Q: Where can I call suspend functions?
22.
Q: Where canI call suspend functions?
A: Inside coroutines and other suspend functions.
Q: How tocreate a coroutine?
A: Use of so-called “coroutine builders”.
31.
- Library functions
-To start a new computation asynchronously:
async { … }
- To start a new computation in a blocking way (often an entry-point):
runBlocking { … }
Coroutine builders
32.
fun processImage() =async {
val image = loadImageAsync().await()
setImage(image)
}
fun loadImageAsync() = async {
/* do the work */
}
Simple “load image” example
33.
async creates anew coroutine:
starts a new asynchronous computation
fun loadImageAsync() = async {
/* do the work */
}
loadImageAsync
fun loadImageAsync(): Deferred<Image>= async {
/* do the work */
}
interface Deferred<out T> {
suspend fun await(): T
}
await is defined as a suspend function
Coroutine can havemany suspension points
suspend fun showUser(email: String) {
val id = login(email)
val user = load(id)
show(user)
}
suspended: login() load()
41.
Suspension points
suspend funshowUser(email: String) {
val id = login(email)
val user = load(id)
show(user)
}
calls of other suspend functions
42.
Q: Which coroutinegets suspended when a
suspend function is called?
A: The one that contains this suspend function.
Call stack ofa coroutine
async
showUser
loadUser
await / library call
suspendCoroutine
suspendCoroutine call
is the language
mechanism to suspend
a given coroutine
45.
Call stack ofa coroutine
async
showUser
loadUser
await / library call
suspendCoroutine
application layer
library layer
language support
46.
Suspended coroutine
async
showUser
loadUser
await /library call
suspendCoroutine
- suspended coroutine is
stored on the heap
- the call stack and values
of all the local variables
are saved
- only one object is used
to store a coroutine
suspend fun foo():Int
suspend fun foo(continuation: Continuation<Int>): Int
“Callbacks” under the hood
Continuation is a generic callback interface:
interface Continuation<in T> {
val context: CoroutineContext
fun resume(value: T)
fun resumeWithException(exception: Throwable)
}
Each suspend function has a hidden parameter:
49.
Q: On whichthread does the coroutine
resume?
A: You specify that.
50.
Run new orresumed coroutine on a thread from the thread pool:
async(Dispatchers.Default) { ... }
Run new or resumed coroutine on the main thread:
async(Dispatchers.Main) { ... }
…
Specify the context
51.
Run Schedule newor resumed coroutine on a thread from the thread
pool:
async(Dispatchers.Default) { ... }
Run Schedule new or resumed coroutine on the main thread:
async(Dispatchers.Main) { ... }
…
Specify the context
fun overlay(image1: Image,image2: Image): Image
suspend fun loadAndOverlay() {
val first = async { loadImage("green") }
val second = async { loadImage("red") }
return overlay(first.await(), second.await())
}
Two asynchronous coroutines
loadAndOverlay
loadImage
loadImage
56.
Q: What happensif an exception is thrown
inside the first child coroutine (during an image
loading)?
A: The second coroutine leaks!
57.
Problem: leaking coroutine
✗
fails
leaks!!!
✗fails
fun overlay(image1: Image, image2: Image): Image
suspend fun loadAndOverlay() {
val first = async { loadImage("green") }
val second = async { loadImage("red") }
return overlay(first.await(), second.await())
}
58.
Solution: introducing localscope
suspend fun loadAndOverlay(): Image =
coroutineScope {
val first = async { loadImage("green") }
val second = async { loadImage("red") }
overlay(first.await(), second.await())
}
✗
fails
catches exception
✗
cancelled
59.
suspend fun loadAndOverlay():Image =
coroutineScope {
val first = async { loadImage("green") }
val second = async { loadImage("red") }
overlay(first.await(), second.await())
}
✗
fails
✗ fails
✗
cancelled
Solution: introducing local scope
60.
Coroutine scope
• waitsfor completion of all the child
coroutines inside this scope
• cancels all the child coroutines if it
gets cancelled (explicitly or by
catching an exception from a child
coroutine)
61.
• You canonly start a new coroutine inside a scope:
Enforcing structure
coroutineScope {
async { ... }
}
GlobalScope.async { ... }
62.
• Each coroutinehas the corresponding scope
GlobalScope.async {
// started in the scope of outer coroutine:
this.async { ... }
}
Enforcing structure
Send & Receive“views” for the same channel
interface SendChannel<in E> {
suspend fun send(element: E)
fun close()
}
interface ReceiveChannel<out E> {
suspend fun receive(): E
}
interface Channel<E> : SendChannel<E>, ReceiveChannel<E>
“Rendezvous” channel semantics
•An element is transferred from sender to receiver only when
send and receive invocations meet in time (“rendezvous”)
• send suspends until another coroutine invokes receive
• receive suspends until another coroutine invokes send
send(task2)
Producer-consumer solution
val channel= Channel<Task>()
waiting for “receive” val task =
channel.receive()
processTask(task)
processing task1
channel.send(task1)
channel.send(task2)
channel.close()
83.
send(task2)
Producer-consumer solution
val channel= Channel<Task>()
receiveRendezvous!
val task =
channel.receive()
processTask(task)
val task =
channel.receive()
processTask(task)
processing task1
channel.send(task1)
channel.send(task2)
channel.close()
84.
Producer-consumer solution
val channel= Channel<Task>()
processing task2
val task =
channel.receive()
processTask(task)
val task =
channel.receive()
processTask(task)
processing task1
channel.send(task1)
channel.send(task2)
channel.close()
85.
consumer #1
producer #1
sendtasks
receive tasks
consumer #2
Producer-consumer solution: many tasks
val channel = Channel<Task>()
86.
Producer-consumer solution: manytasks
val channel = Channel<Task>()
async {
for (i in 1..N) {
channel.send(Task("task$i"))
}
channel.close()
}
producer
87.
Producer-consumer solution: manytasks
val channel = Channel<Task>()
...
async { worker(channel) }
async { worker(channel) }
consumer #1
consumer #2
suspend fun worker(channel: Channel<Task>) {
for (task in channel) {
processTask(task)
}
}
calls receive while iterating