Coroutines are lightweight threads in Kotlin for asynchronous programming, available through the kotlinx.coroutines library. According to JetBrains Kotlin Documentation, 2026, Coroutines allow suspending function execution without blocking a thread, unlike traditional Threads. Coroutines run on a limited thread pool, making them a thousand times lighter than native threads. Kotlin Coroutines are fully integrated with Android Jetpack, Retrofit, Room and other popular libraries of the Android ecosystem.
Key Takeaways
Coroutines are an asynchronous programming mechanism in Kotlin, implemented in the kotlinx.coroutines library. Unlike operating system threads, coroutines are not tied to a specific thread: they can suspend on one thread and resume on another. A single thread can execute thousands of coroutines, switching between them with minimal overhead.
Coroutines appeared in Kotlin 1.3 (2018) as an experimental feature and became stable in Kotlin 1.5 (2021). Coroutines solve the callback hell problem similar to async/await, but provide a richer API: channels (Channel), Flow, exception handling in the Job hierarchy, and direct integration with Android Lifecycle.
According to JetBrains (2025), each coroutine consumes about 100 bytes of memory versus 1+ MB for a native thread. This allows running millions of coroutines in a single application without the risk of OutOfMemoryError. It is the lightweight nature of coroutines that makes them the preferred tool for asynchronicity in Android.
Each Kotlin coroutine is compiled into a state machine using Continuation Passing Style (CPS). The compiler adds a hidden Continuation parameter to each suspend function. The Continuation contains the resumption point and all local variables. When a coroutine suspends, the runtime saves the Continuation, and when it resumes, it restores it on any available thread from the Dispatcher pool.
suspend is a Kotlin keyword that marks a function as suspending. Such a function can only be called from another suspend function or from a coroutine. Inside a suspend function, you can call other suspend functions in any order, and each call point is a potential suspension point.
The mechanics are simple: when a suspend function calls another suspend function, it suspends at that point, freeing the thread. After the called function completes, the runtime continues execution from the saved location. This is called cooperative cancellation — no threads are blocked.
Important: a suspend function is not asynchronous by default. The execution order remains sequential if launch or async are not used. suspend only allows the function to be paused without blocking the thread and to be part of the coroutine context. Continuation Passing Style is a compilation model where each suspend function receives a hidden Continuation callback, and the compiler generates a state machine to manage suspensions and resumptions.
CoroutineScope is a context that defines the lifecycle of coroutines. All coroutines must be launched within a scope. When a scope is cancelled (e.g., when an Activity finishes), all its child coroutines are automatically cancelled. This prevents background task leaks. Android Jetpack provides ready-made scopes for each component: viewModelScope for ViewModel and lifecycleScope for Activity and Fragment, which are automatically cancelled when the corresponding component is destroyed.
Structured Concurrency is a principle that guarantees a coroutine will not complete until all its child coroutines have completed. The Job hierarchy forms a tree: a root coroutine creates a parent job, children create child jobs. Cancelling a parent job propagates to all children. Structured Concurrency is a fundamental difference between coroutines and threads.
| Scope | Where Used | Cancellation |
|---|---|---|
| GlobalScope | Daemon tasks only | Not cancelled automatically |
| viewModelScope | Android ViewModel | On ViewModel clearing |
| lifecycleScope | Android Activity/Fragment | On lifecycle destruction |
| coroutineScope | Inside suspend function | On parent job cancellation |
A regular Job cancels all siblings when one child coroutine fails. SupervisorJob is an exception: a failure in one child coroutine does not affect the others. This is important when several independent tasks are executed in parallel, and one of them may crash without needing to cancel the others.
Dispatchers determine which threads execute coroutines. Dispatchers.Main — the main Android UI thread. Dispatchers.IO — a pool for blocking operations (network, disk). Dispatchers.Default — for CPU-intensive tasks. Dispatchers.Unconfined — starts in the current thread but does not guarantee to stay on it. Choosing the right Dispatcher is critical for performance: an IO task on Default will block the compute pool, while a CPU task on IO will create unnecessary threads.
withContext — a function for switching Dispatchers inside a coroutine. For example, a suspend function parsing JSON can switch to Dispatchers.Default for computation and return to Dispatchers.Main for UI updates. withContext is the most commonly used builder in Android development.
launch — launches a coroutine, returns a Job, does not return a result (fire-and-forget). async — launches a coroutine, returns a Deferred from which the result can be obtained via await. runBlocking — blocks the current thread to execute a coroutine (only for tests and main functions). Choosing a builder depends on the scenario: launch is suitable for events and updates, async for tasks with a result, runBlocking only for tests or entry points.
Let us consider three practical scenarios: a basic coroutine with launch, a parallel call with async, and error handling with SupervisorJob.
viewModelScope.launch launches a coroutine in the ViewModel context. When the ViewModel is cleared, the coroutine is automatically cancelled.
class ProfileViewModel : ViewModel() {
fun loadUser() {
viewModelScope.launch(Dispatchers.IO) {
val user = api.fetchUser()
withContext(Dispatchers.Main) {
showUser(user)
}
}
}
}
coroutineScope with async launches three requests in parallel. Results are collected via .await(). If any request fails, all are cancelled.
suspend fun loadDashboard(): Dashboard = coroutineScope {
val user = async { api.fetchUser() }
val posts = async { api.fetchPosts() }
val stats = async { api.fetchStats() }
Dashboard(user.await(), posts.await(), stats.await())
}
SupervisorJob allows each coroutine to complete independently. An error in one request does not cancel the others.
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scope.launch {
try { api.fetchUsers() } catch (e: Exception) { log(e) }
}
scope.launch {
try { api.fetchPosts() } catch (e: Exception) { log(e) }
}
Threads are an operating system primitive. Each thread has its own stack (~1 MB) and requires a system call for creation and switching. Coroutines are a language primitive, not tied to the OS. They use Continuation to save state and switch at the runtime level without system calls.
According to Google (2025), using coroutines instead of threads reduces memory consumption for background tasks in Android applications by 90–95%. All modern Android libraries (Retrofit, Room, WorkManager) have built-in coroutine support through suspend functions. Ktor (JetBrains HTTP client framework) is also built entirely on coroutines, providing suspend functions for each request without a callback API. Room supports coroutines through suspend functions in DAO, allowing database queries without blocking the main thread.
Threads remain necessary for native code through JNI, long-running CPU-intensive blocking calls (video rendering, simulations), and when integrating with C libraries. For everything else — coroutines.
Frequently Asked Questions
Coroutine is a suspendable unit of work that executes on an existing thread. A thread is a system resource with its own stack. Coroutines are thousands of times lighter than threads and do not block resources when suspended.
Dispatchers.IO is designed for blocking I/O operations (network, files) and can create new threads when necessary. Dispatchers.Default has a fixed-size pool (number of CPU cores) for CPU-intensive computations.
Job.cancel() cancels the coroutine and all its children. To check for cancellation inside a coroutine, use ensureActive() — it throws a CancellationException if the coroutine is cancelled.
Yes — through the kotlinx-coroutines-rx3 library. It provides awaitSingle, awaitFirst and other functions to convert Observable/Single to suspend functions and back via flowable.
Flow is a cold asynchronous data stream, the coroutine equivalent of RxJava Observable. Flow emits values sequentially and completes with an exception or success. It supports map, filter, catch and other operators.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also