Coroutines
The coroutine API is oficial multiplatform Kotlin library that provides way to perform tasks asynchronously. It has:
- coroutine builders (launch, async)
- scope builders (coroutineScope, supervisorScope, withContext, and withTimeout)
- communication and synchronization primitives (Mutex, Semaphore)
- cold asynchronous stream with flow builder and comprehensive operator set (filter, map, etc)
- ui support to provide the Main dispatcher for various single-threaded UI libraries: Android, JavaFX, and Swing.
- platform specific support
suspend fun main() = coroutineScope {
launch {
delay(1000)
println("Kotlin Coroutines World!")
}
println("Hello")
}
dependencies
For using coroutines you need add dependencies to your project
// In order to work with Main dispatcher, the
// following artifacts should be added to project:
// - kotlinx-coroutines-android for Android Main thread dispatcher
// - kotlinx-coroutines-javafx for JavaFx Application thread dispatcher
// - kotlinx-coroutines-swing for Swing EDT dispatcher
// - kotlinx-coroutines-test for testing purpose
dependencies {
// need kotlin version 1.9.21
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1-Beta")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1-Beta")
// ...
}
Core modules of kotlinx.coroutines are also available for Kotlin/JS and Kotlin/Native.
For multiplatform projects, you can add a dependency to kotlinx-coroutines-core right to the commonMain source set:
commonMain {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1-Beta")
}
}
basics
Coroutine is lightweight task that can be suspend (paused) and resume executing. Thus coroutine does not block current thread. For example, you can run many coroutines (100k) on single Java thread. If you try do it with threads only you may get out-of-memory error.
// will print all dots within 5 seconds.
fun main() = runBlocking {
repeat(100_000) { // launch a lot of coroutines
launch {
delay(5000L)
print(".")
}
}
}
Scope helps to define a lifecycle of coroutine. For example, using Android built-in scopes, you can safely run the coroutine in a Fragment. When user closes fragment before completion, the coroutine will be canceled.
Coroutine dispatcher determines which thread or threads used to execute coroutines. On JVM platform you can convert the executor to the dispatcher and vice versa.
Coroutine context is a set of various elements like scopes, Job instance and dispatcher.
The Job class represents the coroutine itself. You can cancel or await execution.
The suspend keyword specifies, that function can be paused and resumed. So they can execute a long running operation and wait for it to complete without blocking. Such functions can be called only within scope or from other suspendable functions.
scopes
The CoroutineScope interface defines a scope for new coroutines.
Every coroutine builder, like launch and async, is an extension on CoroutineScope and inherits its coroutineContext to automatically propagate all its elements and cancellation.
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
fun myAsyncTask(){
scope.launch {
// ...
}
}
The best ways to obtain a standalone instance of the scope are CoroutineScope() and MainScope() factory functions. Additional context elements can be appended to the scope using the plus operator.
//Create the main CoroutineScope for UI components.
class MyAndroidActivity {
private val scope = MainScope()
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
}
The coroutineScope function creates a CoroutineScope and calls the specified suspend block with this scope. The provided scope inherits its coroutineContext from the outer scope, but overrides the context's Job. This function is designed for parallel decomposition of work.
suspend fun showSomeData() = coroutineScope {
val data = async(Dispatchers.IO) { // <- extension on current scope
... load some UI data for the Main thread ...
}
withContext(Dispatchers.Main) {
doSomeWork()
val result = data.await()
display(result)
}
}
coroutine builders
function | description |
---|---|
CoroutineScope.launch(ctx, start, block) | Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a Job. The coroutine is cancelled when the resulting job is cancelled. |
CoroutineScope.async(ctx, start, block) | Creates a coroutine and returns its future result as an implementation of Deferred. The running coroutine is cancelled when the resulting deferred is cancelled. By defualt, it cancels the parent job or outer scope on failure to enforce structured concurrency paradigm. To change that behaviour, supervising parent (SupervisorJob or supervisorScope) can be used. |
runBlocking(ctx, block) | Runs a new coroutine and blocks the current thread interruptibly until its completion. This function should not be used from a coroutine. It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in main functions and in tests. |
By default EmptyCoroutineContext is used as coroutine context.
If context does not have any dispatcher, Dispatchers.Default is used.
CoroutineStart enum has following values:
- DEFAULT - immediately schedules the coroutine for execution according to its context
- LAZY - starts the coroutine lazily, only when it is needed
- ATOMIC - this is similar to DEFAULT, but the coroutine cannot be cancelled before it starts executing.
- UNDISPATCHED - immediately executes the coroutine until its first suspension point in the current thread similarly to the coroutine being started using Dispatchers.Unconfined. However, when the coroutine is resumed from suspension it is dispatched according to the CoroutineDispatcher in its context.
runBlocking {
// val value1 : Deferred<Int> = ...
val value1 = async(context = Dispatchers.IO) {
// .. return value
}
val value2 = async{
// .. return value
}
println("Results:\n${value1.await()}\n${value2.await()}")
}
dispatchers
val mainDispatcher = try {
Dispatchers.Main.apply { isDispatchNeeded(this) }
} catch (e: IllegalStateException) { // emulate main thread
Executors.newFixedThreadPool(1) {
Thread(it, "Main thread emulated")
}.asCoroutineDispatcher()
}
class | description |
---|---|
CoroutineDispatcher | Base class for coroutine dispatchers. |
ExecutorCoroutineDispatcher | A bridge between coroutine-based API and asynchronous API that requires an instance of the Executor. |
Dispatchers | Holds default dispatchers:
|
change thread
The withContext() function allows you to change the thread in which the coroutine will executed. After the function completes, the coroutine will continue execution in the previous thread.
In other words, this function does not create new coroutine, but changes thread where current coroutine is executed.
runBlocking { println("Result:") // launch new coroutine launch(Dispatchers.Default) { println("launch coroutine in thread ${Thread.currentThread().name}") withContext(mainDispatcher){ println("jump to thread ${Thread.currentThread().name}") } println("jump back to thread ${Thread.currentThread().name}") } }
Result: launch coroutine in thread DefaultDispatcher-worker-2 @coroutine#2 jump to thread Main thread emulated @coroutine#2 jump back to thread DefaultDispatcher-worker-2 @coroutine#2