Threads vs Coroutines in Kotlin
Threads vs Coroutines in Kotlin
Threads and coroutines both enable concurrent execution, but they differ fundamentally in how they are managed, how they handle blocking, and what resources they consume. Threads are operating system level constructs with their own call stacks, managed by the OS scheduler. Coroutines are language level constructs managed by the Kotlin runtime that suspend without blocking the underlying thread. This difference in resource overhead is why coroutines are often called lightweight threads. By the end of this lesson, you will be able to:
- Explain how threads are managed by the operating system and what resources each thread consumes.
- Explain how coroutines suspend and resume without blocking the underlying thread.
- Describe the role of
CoroutineDispatcherin mapping coroutines to threads. - Explain why many coroutines can run concurrently on a small pool of threads.
- Choose between threads and coroutines based on the nature of the concurrent work.
Threads
A thread is a distinct execution path managed by the operating system. Each thread has its own call stack, typically consuming around 1 MB of memory. The OS scheduler decides when each thread runs, and switching between threads (context switching) involves saving and restoring register state, which adds overhead.
When a thread performs a blocking operation like network I/O or disk access, the entire thread pauses until the operation completes. No other work can run on that thread during the wait. If you need to handle many concurrent blocking operations, you need many threads, and each one consumes memory and adds scheduling overhead.
Creating thousands of threads to handle thousands of concurrent connections is possible but expensive. The memory footprint grows linearly, and the OS scheduler spends increasing time managing context switches as the thread count rises.
Coroutines
A coroutine is a computation that can suspend at specific points and resume later. When a coroutine reaches a suspend function like delay(), withContext(), or a network call using a suspending API, it releases the thread it was running on. That thread becomes available to run other coroutines immediately.
The Kotlin runtime manages coroutine scheduling. Suspending a coroutine saves only a small continuation object, not an entire call stack. Resuming a coroutine restores this continuation on whatever thread the dispatcher assigns. This means a single thread can drive thousands of coroutines by switching between them at suspension points.
suspend fun fetchData(): String {
delay(1000) // suspends, releases the thread
return "result"
}
During the 1 second delay, the thread is free to execute other coroutines. No thread is blocked or consumed while waiting.
The suspend keyword marks a function as a suspension point. The compiler transforms it into a state machine with continuations. Each continuation captures the local state at the suspension point so the coroutine can resume exactly where it left off. This transformation is invisible to the developer, who writes sequential code that the compiler converts to non-blocking execution.
Structured Concurrency
Coroutines introduce structured concurrency, where every coroutine runs within a scope that defines its lifetime. When a scope is cancelled, all coroutines within it are cancelled. When a coroutine fails, its parent scope can decide how to handle the failure. This hierarchical structure prevents resource leaks and orphaned tasks that are common with raw thread management.
This interview continues for subscribers
Subscribe to Dove Letter for full access to exclusive interviews about Android and Kotlin development.
Become a Sponsor