[2026] Kotlin Coroutines vs Threads | Concurrency Model, Cost, and When to Use Which
이 글의 핵심
Complete comparison of Kotlin coroutines and OS threads: lightweight concurrency, memory, scheduling, structured concurrency, Dispatchers, performance benchmarks, real-world examples, and best practices.
Introduction
“Coroutines or threads?” This article compares Kotlin coroutines and Java/OS threads and gives practical defaults.
What you will learn
- How each works (kernel vs user-level scheduling)
- Cost of creation and context switches
- Structured concurrency and cancellation
- When threads still appear
- Performance benchmarks
- Real-world examples
Table of contents
- Quick comparison
- How they work
- Performance benchmarks
- Memory overhead
- Structured concurrency
- Dispatchers explained
- Real-world examples
- Common mistakes
- Practical guide
- Side-by-side code
- Best practices
1. Quick comparison
| Coroutines | Threads | |
|---|---|---|
| Weight | Many thousands+ | Dozens–hundreds typical |
| Memory | ~KB-scale state | ~MB stacks |
| Create cost | Very low (~1-2μs) | Higher (~100-200μs OS) |
| Context switch | User-space (cheap) | Kernel (heavier) |
| Cancellation | Structured + cooperative | Manual / interrupt |
| Default | Prefer | Legacy / special |
| Scheduling | Dispatchers (thread pools) | OS scheduler |
| Blocking | Suspends without blocking | Blocks thread |
2. How they work
Threads
Threads: OS schedules, large stacks (typically 1MB), kernel transitions for context switches. 아래 코드는 kotlin를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// Traditional thread
// 실행 예제
Thread {
println("Running on: ${Thread.currentThread().name}")
Thread.sleep(1000) // Blocks entire thread
println("Done")
}.start()
Characteristics:
- Scheduled by OS kernel
- Pre-emptive multitasking
- Each thread has its own stack (1MB default on Linux)
- Context switch involves kernel mode transition
Coroutines
Coroutines: suspend without blocking threads; delay frees the worker; state machines resume later—often on thread pools (Dispatchers).
아래 코드는 kotlin를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// Coroutine
// 실행 예제
GlobalScope.launch {
println("Running on: ${Thread.currentThread().name}")
delay(1000) // Suspends, thread is free
println("Done")
}
Characteristics:
- Cooperative multitasking
- Suspend functions release thread
- State machine transformation by compiler
- Resumed on dispatcher thread pool
Under the hood
다음은 kotlin를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// This coroutine code:
suspend fun example() {
val result1 = fetchData()
val result2 = processData(result1)
return result2
}
// Becomes state machine (simplified):
class ExampleStateMachine : Continuation<Unit> {
var label = 0
var result1: Data? = null
override fun resumeWith(result: Result<Any?>) {
when (label) {
0 -> {
label = 1
fetchData(this) // Pass continuation
}
1 -> {
result1 = result.getOrThrow() as Data
label = 2
processData(result1, this)
}
2 -> {
// Done
}
}
}
}
3. Performance benchmarks
Creation cost
다음은 kotlin를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
import kotlin.system.measureTimeMillis
fun benchmarkThreads() {
val time = measureTimeMillis {
repeat(10_000) {
Thread {
// Do nothing
}.start()
}
}
println("Threads: ${time}ms") // ~2000-3000ms
}
fun benchmarkCoroutines() = runBlocking {
val time = measureTimeMillis {
repeat(10_000) {
launch {
// Do nothing
}
}
}
println("Coroutines: ${time}ms") // ~50-100ms
}
Results (typical):
- 10,000 threads: 2-3 seconds
- 10,000 coroutines: 50-100ms
- 20-60x faster for coroutines
Context switch cost
다음은 kotlin를 활용한 상세한 구현 코드입니다. 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
fun benchmarkThreadContextSwitch() {
val threads = List(1000) {
Thread {
repeat(1000) {
Thread.yield()
}
}
}
val time = measureTimeMillis {
threads.forEach { it.start() }
threads.forEach { it.join() }
}
println("Thread switches: ${time}ms") // ~5000-10000ms
}
fun benchmarkCoroutineContextSwitch() = runBlocking {
val time = measureTimeMillis {
repeat(1000) {
launch {
repeat(1000) {
yield()
}
}
}
}
println("Coroutine switches: ${time}ms") // ~500-1000ms
}
Concurrent I/O operations
다음은 kotlin를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// 100,000 concurrent HTTP requests
fun withThreads() {
val executor = Executors.newFixedThreadPool(1000) // Limited pool
val time = measureTimeMillis {
repeat(100_000) {
executor.submit {
// Simulate HTTP call
Thread.sleep(100)
}
}
executor.shutdown()
executor.awaitTermination(1, TimeUnit.HOURS)
}
println("Threads: ${time}ms") // ~10000ms (limited by pool size)
}
fun withCoroutines() = runBlocking {
val time = measureTimeMillis {
repeat(100_000) {
launch(Dispatchers.IO) {
delay(100)
}
}
}
println("Coroutines: ${time}ms") // ~100-200ms
}
4. Memory overhead
Thread memory
아래 코드는 kotlin를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
fun threadMemory() {
val runtime = Runtime.getRuntime()
val before = runtime.totalMemory() - runtime.freeMemory()
val threads = List(1000) {
Thread {
Thread.sleep(10000)
}.apply { start() }
}
val after = runtime.totalMemory() - runtime.freeMemory()
println("Memory per thread: ${(after - before) / 1000 / 1024}KB")
// ~1024KB (1MB) per thread
}
Coroutine memory
아래 코드는 kotlin를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
fun coroutineMemory() = runBlocking {
val runtime = Runtime.getRuntime()
val before = runtime.totalMemory() - runtime.freeMemory()
repeat(1000) {
launch {
delay(10000)
}
}
val after = runtime.totalMemory() - runtime.freeMemory()
println("Memory per coroutine: ${(after - before) / 1000}bytes")
// ~1-2KB per coroutine
}
Summary:
- Thread: ~1MB per thread (stack size)
- Coroutine: ~1-2KB per suspended coroutine
- 500-1000x more memory efficient
5. Structured concurrency
Without structured concurrency (threads)
다음은 kotlin를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
fun fetchUserData(userId: String): User {
val thread1 = Thread {
// Fetch profile
}
val thread2 = Thread {
// Fetch posts
}
thread1.start()
thread2.start()
// What if one fails? How to cancel both?
// What if parent is cancelled?
thread1.join()
thread2.join()
}
With structured concurrency (coroutines)
아래 코드는 kotlin를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
suspend fun fetchUserData(userId: String): User = coroutineScope {
val profile = async { fetchProfile(userId) }
val posts = async { fetchPosts(userId) }
// If one fails, both are cancelled
// If parent is cancelled, both are cancelled
User(profile.await(), posts.await())
}
Cancellation propagation
아래 코드는 kotlin를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
val job = GlobalScope.launch {
val child1 = launch {
delay(1000)
println("Child 1")
}
val child2 = launch {
delay(2000)
println("Child 2")
}
delay(500)
}
delay(100)
job.cancel() // Cancels parent and all children
6. Dispatchers explained
Dispatchers.Default
다음은 간단한 kotlin 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// CPU-bound work
launch(Dispatchers.Default) {
val result = heavyComputation()
}
- Thread pool size: Number of CPU cores
- Use for: CPU-intensive tasks
- Examples: Sorting, parsing, compression
Dispatchers.IO
아래 코드는 kotlin를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// I/O-bound work
launch(Dispatchers.IO) {
val data = database.query()
val response = httpClient.get(url)
}
- Thread pool size: 64 (or configured max)
- Use for: Network, disk, database
- Examples: HTTP requests, file I/O, database queries
Dispatchers.Main
다음은 간단한 kotlin 코드 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// Android UI thread
launch(Dispatchers.Main) {
textView.text = "Updated"
}
- Single thread (UI thread)
- Use for: UI updates
- Platform-specific (Android, JavaFX, Swing)
Custom dispatcher
아래 코드는 kotlin를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
val customDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
launch(customDispatcher) {
// Custom thread pool
}
7. Real-world examples
Example 1: Parallel API calls
다음은 kotlin를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// With threads
fun fetchDataThreads(): Result {
val executor = Executors.newFixedThreadPool(3)
val future1 = executor.submit { api.getUsers() }
val future2 = executor.submit { api.getPosts() }
val future3 = executor.submit { api.getComments() }
val users = future1.get()
val posts = future2.get()
val comments = future3.get()
executor.shutdown()
return Result(users, posts, comments)
}
// With coroutines
suspend fun fetchDataCoroutines(): Result = coroutineScope {
val users = async { api.getUsers() }
val posts = async { api.getPosts() }
val comments = async { api.getComments() }
Result(users.await(), posts.await(), comments.await())
}
Example 2: Producer-consumer
다음은 kotlin를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// With threads
class ThreadProducerConsumer {
val queue = LinkedBlockingQueue<Int>()
fun start() {
Thread {
repeat(100) {
queue.put(it)
Thread.sleep(10)
}
}.start()
Thread {
while (true) {
val item = queue.take()
process(item)
}
}.start()
}
}
// With coroutines
class CoroutineProducerConsumer {
val channel = Channel<Int>()
fun start() = CoroutineScope(Dispatchers.Default).launch {
launch {
repeat(100) {
channel.send(it)
delay(10)
}
channel.close()
}
launch {
for (item in channel) {
process(item)
}
}
}
}
Example 3: Timeout handling
다음은 kotlin를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// With threads (complex)
fun fetchWithTimeoutThread(url: String): String? {
val future = executor.submit { httpClient.get(url) }
return try {
future.get(5, TimeUnit.SECONDS)
} catch (e: TimeoutException) {
future.cancel(true)
null
}
}
// With coroutines (simple)
suspend fun fetchWithTimeoutCoroutine(url: String): String? {
return withTimeoutOrNull(5000) {
httpClient.get(url)
}
}
8. Common mistakes
Mistake 1: Blocking in coroutine
아래 코드는 kotlin를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ BAD: Blocks thread
launch(Dispatchers.Default) {
Thread.sleep(1000) // Blocks thread!
}
// ✅ GOOD: Suspends
launch(Dispatchers.Default) {
delay(1000) // Suspends, thread is free
}
Mistake 2: Using GlobalScope
다음은 kotlin를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ BAD: No lifecycle management
GlobalScope.launch {
// Runs forever, no cancellation
}
// ✅ GOOD: Scoped
class MyActivity : CoroutineScope {
override val coroutineContext = Dispatchers.Main + Job()
fun loadData() {
launch {
// Cancelled when activity is destroyed
}
}
fun onDestroy() {
coroutineContext.cancel()
}
}
Mistake 3: Wrong dispatcher
아래 코드는 kotlin를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ❌ BAD: CPU work on IO dispatcher
launch(Dispatchers.IO) {
val result = heavyComputation() // Wastes IO thread
}
// ✅ GOOD: Use Default for CPU work
launch(Dispatchers.Default) {
val result = heavyComputation()
}
Mistake 4: Not handling exceptions
다음은 kotlin를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ BAD: Exception kills coroutine silently
launch {
throw Exception("Error") // Lost!
}
// ✅ GOOD: Handle exceptions
launch {
try {
riskyOperation()
} catch (e: Exception) {
handleError(e)
}
}
// Or use CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught: $exception")
}
launch(handler) {
throw Exception("Error")
}
9. Practical guide
When to use coroutines
✅ Use coroutines for:
- Network / DB (
Dispatchers.IO) - Parallel
async/await - Composable concurrency with clear scopes
- Android UI operations
- Sequential async operations
- Thousands of concurrent tasks
When to use threads
⚠️ Threads may remain for:
- Legacy Java pools
- Blocking code you cannot wrap yet
- Interop constraints
- Libraries that require ExecutorService
- Very simple one-off tasks
Decision flowchart
아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Need concurrency?
├─ Yes
│ ├─ Kotlin project?
│ │ ├─ Yes → Use coroutines
│ │ └─ No → Use threads/executors
│ └─ Legacy Java?
│ └─ Use threads/executors
└─ No → Sequential code
10. Side-by-side code
Fetching multiple URLs
다음은 kotlin를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 반복문으로 데이터를 처리합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// Threads
fun fetchUrlsThreads(urls: List<String>): List<String> {
val executor = Executors.newFixedThreadPool(10)
val futures = urls.map { url ->
executor.submit<String> {
httpClient.get(url)
}
}
val results = futures.map { it.get() }
executor.shutdown()
return results
}
// Coroutines
suspend fun fetchUrlsCoroutines(urls: List<String>): List<String> {
return coroutineScope {
urls.map { url ->
async(Dispatchers.IO) {
httpClient.get(url)
}
}.awaitAll()
}
}
Retry logic
다음은 kotlin를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// Threads (complex)
fun retryThread(maxAttempts: Int, block: () -> String): String {
repeat(maxAttempts) { attempt ->
try {
return block()
} catch (e: Exception) {
if (attempt == maxAttempts - 1) throw e
Thread.sleep(1000 * (attempt + 1))
}
}
throw IllegalStateException()
}
// Coroutines (simple)
suspend fun retryCoroutine(maxAttempts: Int, block: suspend () -> String): String {
repeat(maxAttempts) { attempt ->
try {
return block()
} catch (e: Exception) {
if (attempt == maxAttempts - 1) throw e
delay(1000 * (attempt + 1))
}
}
throw IllegalStateException()
}
11. Best practices
1. Use structured concurrency
아래 코드는 kotlin를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// ✅ GOOD: Scoped
suspend fun loadUserData() = coroutineScope {
val profile = async { fetchProfile() }
val posts = async { fetchPosts() }
UserData(profile.await(), posts.await())
}
2. Choose correct dispatcher
아래 코드는 kotlin를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// CPU-bound
launch(Dispatchers.Default) {
val result = complexCalculation()
}
// I/O-bound
launch(Dispatchers.IO) {
val data = database.query()
}
// UI updates
launch(Dispatchers.Main) {
updateUI(data)
}
3. Handle cancellation
아래 코드는 kotlin를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
suspend fun longRunningTask() {
repeat(1000) { i ->
ensureActive() // Check cancellation
processItem(i)
}
}
4. Use withContext for switching
아래 코드는 kotlin를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
suspend fun loadData(): Data {
val data = withContext(Dispatchers.IO) {
database.query()
}
// Back to original dispatcher
return processData(data)
}
5. Avoid GlobalScope
아래 코드는 kotlin를 사용한 구현 예제입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
// ❌ BAD
GlobalScope.launch { }
// ✅ GOOD
class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
// Cancelled when ViewModel is cleared
}
}
}
Summary
Key takeaways
- Default to coroutines on the JVM with Kotlin
- Threads for narrow legacy/interop cases
- Pick Dispatchers for I/O vs CPU
- Use structured concurrency for lifecycles
- Coroutines are 20-60x faster to create
- 500-1000x more memory efficient than threads
Performance summary
| Metric | Threads | Coroutines | Winner |
|---|---|---|---|
| Creation | ~100-200μs | ~1-2μs | Coroutines (100x) |
| Memory | ~1MB | ~1-2KB | Coroutines (500x) |
| Context switch | Kernel | User-space | Coroutines |
| Scalability | Hundreds | Millions | Coroutines |
| Coroutines are not magic—they organize async work safely on top of threads. |
Related posts
Keywords
Kotlin, coroutine, thread, async, concurrency, Dispatchers, structured concurrency, comparison, performance