Coroutines basics
To create applications that perform multiple tasks at once, a concept known as concurrency, Kotlin uses coroutines. A coroutine is a suspendable computation that lets you write concurrent code in a clear, sequential style. Coroutines can run concurrently with other coroutines and potentially in parallel.
On the JVM and in Kotlin/Native, all concurrent code, such as coroutines, runs on threads, managed by the operating system. Coroutines can suspend their execution instead of blocking a thread. This allows one coroutine to suspend while waiting for some data to arrive and another coroutine to run on the same thread, ensuring effective resource utilization.
For more information about the differences between coroutines and threads, see Comparing coroutines and JVM threads.
Suspending functions
The most basic building block of coroutines is the suspending function. It allows a running operation to pause and resume later without affecting the structure of your code.
To declare a suspending function, use the suspend
keyword:
You can only call a suspending function from another suspending function. To call suspending functions at the entry point of a Kotlin application, mark the main()
function with the suspend
keyword:
This example doesn't use concurrency yet, but by marking the functions with the suspend
keyword, you allow them to call other suspending functions and run concurrent code inside.
While the suspend
keyword is part of the core Kotlin language, most coroutine features are available through the kotlinx.coroutines
library.
Add the kotlinx.coroutines library to your project
To include the kotlinx.coroutines
library in your project, add the corresponding dependency configuration based on your build tool:
Create your first coroutines
To create a coroutine in Kotlin, you need the following:
A coroutine scope in which it can run, for example inside the
withContext()
function.A coroutine builder like
CoroutineScope.launch()
to start it.A dispatcher to control which threads it uses.
Let's look at an example that uses multiple coroutines in a multithreaded environment:
Import the
kotlinx.coroutines
library:import kotlinx.coroutines.*Mark functions that can pause and resume with the
suspend
keyword:suspend fun greet() { println("The greet() on the thread: ${Thread.currentThread().name}") } suspend fun main() {}Add the
delay()
function to simulate a suspending task, such as fetching data or writing to a database:suspend fun greet() { println("The greet() on the thread: ${Thread.currentThread().name}") delay(1000L) }Use
withContext(Dispatchers.Default)
to define an entry point for multithreaded concurrent code that runs on a shared thread pool:suspend fun main() { withContext(Dispatchers.Default) { // Add the coroutine builders here } }Use a coroutine builder function like
CoroutineScope.launch()
to start the coroutine:suspend fun main() { withContext(Dispatchers.Default) { // this: CoroutineScope // Starts a coroutine inside the scope with CoroutineScope.launch() this.launch { greet() } println("The withContext() on the thread: ${Thread.currentThread().name}") } }Combine these pieces to run multiple coroutines at the same time on a shared pool of threads:
// Imports the coroutines library import kotlinx.coroutines.* // Imports the kotlin.time.Duration to express duration in seconds import kotlin.time.Duration.Companion.seconds // Defines a suspending function suspend fun greet() { println("The greet() on the thread: ${Thread.currentThread().name}") // Suspends for 1 second and releases the thread delay(1.seconds) // The delay() function simulates a suspending API call here // You can add suspending API calls here like a network request } suspend fun main() { // Runs the code inside this block on a shared thread pool withContext(Dispatchers.Default) { // this: CoroutineScope this.launch() { greet() } // Starts another coroutine this.launch() { println("The CoroutineScope.launch() on the thread: ${Thread.currentThread().name}") delay(1.seconds) // The delay function simulates a suspending API call here // You can add suspending API calls here like a network request } println("The withContext() on the thread: ${Thread.currentThread().name}") } }
Try running the example multiple times. You may notice that the output order and thread names may change each time you run the program, because the OS decides when threads run.
Coroutine scope and structured concurrency
When you run many coroutines in an application, you need a way to manage them as groups. Kotlin coroutines rely on a principle called structured concurrency to provide this structure.
According to this principle, coroutines form a tree hierarchy of parent and child tasks with linked lifecycles. A coroutine's lifecycle is the sequence of states from its creation until completion, failure, or cancellation.
A parent coroutine waits for its children to complete before it finishes. If the parent coroutine fails or gets canceled, all its child coroutines are recursively canceled too. Keeping coroutines connected this way makes cancellation and error handling predictable and safe.
To maintain structured concurrency, new coroutines can only be launched in a CoroutineScope
that defines and manages their lifecycle. The CoroutineScope
includes the coroutine context, which defines the dispatcher and other execution properties. When you start a coroutine inside another coroutine, it automatically becomes a child of its parent scope.
Calling a coroutine builder function, such as CoroutineScope.launch()
on a CoroutineScope
, starts a child coroutine of the coroutine associated with that scope. Inside the builder's block, the receiver is a nested CoroutineScope
, so any coroutines you launch there become its children.
Create a coroutine scope with the coroutineScope()
function
To create a new coroutine scope with the current coroutine context, use the coroutineScope()
function. This function creates a root coroutine of the coroutine subtree. It's the direct parent of coroutines launched inside the block and the indirect parent of any coroutines they launch. coroutineScope()
executes the suspending block and waits until the block and any coroutines launched in it complete.
Here's an example:
Since no dispatcher is specified in this example, the CoroutineScope.launch()
builder functions in the coroutineScope()
block inherit the current context. If that context doesn't have a specified dispatcher, CoroutineScope.launch()
uses Dispatchers.Default
, which runs on a shared pool of threads.
Extract coroutine builders from the coroutine scope
In some cases, you may want to extract coroutine builder calls, such as CoroutineScope.launch()
, into separate functions.
Consider the following example:
The coroutineScope()
function takes a lambda with a CoroutineScope
receiver. Inside this lambda, the implicit receiver is a CoroutineScope
, so builder functions like CoroutineScope.launch()
and CoroutineScope.async()
resolve as extension functions on that receiver.
To extract the coroutine builders into another function, that function must declare a CoroutineScope
receiver, otherwise a compilation error occurs:
Coroutine builder functions
A coroutine builder function is a function that accepts a suspend
lambda that defines a coroutine to run. Here are some examples:
Coroutine builder functions require a CoroutineScope
to run in. This can be an existing scope or one you create with helper functions such as coroutineScope()
, runBlocking()
, or withContext()
. Each builder defines how the coroutine starts and how you interact with its result.
CoroutineScope.launch()
The CoroutineScope.launch()
coroutine builder function is an extension function on CoroutineScope
. It starts a new coroutine without blocking the rest of the scope, inside an existing coroutine scope.
Use CoroutineScope.launch()
to run a task alongside other work when the result isn't needed or you don't want to wait for it:
After running this example, you can see that the main()
function isn't blocked by CoroutineScope.launch()
and keeps running other code while the coroutine works in the background.
CoroutineScope.async()
The CoroutineScope.async()
coroutine builder function is an extension function on CoroutineScope
. It starts a concurrent computation inside an existing coroutine scope and returns a Deferred
handle that represents an eventual result. Use the .await()
function to suspend the code until the result is ready:
runBlocking()
The runBlocking()
coroutine builder function creates a coroutine scope and blocks the current thread until the coroutines launched in that scope finish.
Use runBlocking()
only when there is no other option to call suspending code from non-suspending code:
Coroutine dispatchers
A coroutine dispatcher controls which thread or thread pool coroutines use for their execution. Coroutines aren't always tied to a single thread. They can pause on one thread and resume on another, depending on the dispatcher. This lets you run many coroutines at the same time without allocating a separate thread for every coroutine.
A dispatcher works together with the coroutine scope to define when coroutines run and where they run. While the coroutine scope controls the coroutine's lifecycle, the dispatcher controls which threads are used for execution.
The kotlinx.coroutines
library includes different dispatchers for different use cases. For example, Dispatchers.Default
runs coroutines on a shared pool of threads, performing work in the background, separate from the main thread. This makes it an ideal choice for CPU-intensive operations like data processing.
To specify a dispatcher for a coroutine builder like CoroutineScope.launch()
, pass it as an argument:
Alternatively, you can use a withContext()
block to run all code in it on a specified dispatcher:
To learn more about coroutine dispatchers and their uses, including other dispatchers like Dispatchers.IO
and Dispatchers.Main
, see Coroutine context and dispatchers.
Comparing coroutines and JVM threads
While coroutines are suspendable computations that run code concurrently like threads on the JVM, they work differently under the hood.
A thread is managed by the operating system. Threads can run tasks in parallel on multiple CPU cores and represent a standard approach to concurrency on the JVM. When you create a thread, the operating system allocates memory for its stack and uses the kernel to switch between threads. This makes threads powerful but also resource-intensive. Each thread usually needs a few megabytes of memory, and typically the JVM can only handle a few thousand threads at once.
On the other hand, a coroutine isn't bound to a specific thread. It can suspend on one thread and resume on another, so many coroutines can share the same thread pool. When a coroutine suspends, the thread isn't blocked and remains free to run other tasks. This makes coroutines much lighter than threads and allows running millions of them in one process without exhausting system resources.
Let's look at an example where 50,000 coroutines each wait five seconds and then print a period (.
):
Now let's look at the same example using JVM threads:
Running this version uses much more memory because each thread needs its own memory stack. For 50,000 threads, that can be up to 100 GB, compared to roughly 500 MB for the same number of coroutines.
Depending on your operating system, JDK version, and settings, the JVM thread version may throw an out-of-memory error or slow down thread creation to avoid running too many threads at once.
What's next
Discover more about combining suspending functions in Composing suspending functions.
Learn how to cancel coroutines and handle timeouts in Cancellation and timeouts.
Dive deeper into coroutine execution and thread management in Coroutine context and dispatchers.
Learn how to return multiple asynchronously computed values in Asynchronous flows.