<코틀린 코루틴의 정석(조세영)> 도서의 "3장. CoroutineDispatcher", "11장. 코루틴 심화"을 참고해서 작성된 글입니다.
요약
CoroutineDispatcher는 코루틴이 실제로 실행될 스레드 또는 스레드풀을 결정하는 역할을 수행합니다.
제한된 디스패처는 특정 스레드나 고정된 스레드풀에서만 실행되도록 하여 순차 처리나 병렬 처리 시 예측 가능성을 높이며, newSingleThreadContext 또는 newFixedThreadPoolContext를 통해 직접 생성할 수 있습니다.
그러나 스레드풀 생성 비용과 관리 부담을 줄이기 위해, 안드로이드 메인 스레드를 위한 Dispatchers.Main, I/O 작업용 Dispatchers.IO, CPU 바운드 작업용 Dispatchers.Default와 같은 사전 정의된 공유 Dispatcher를 활용하는 것이 권장됩니다.
1. CoroutineDispatcher
CoroutineDispatcher
코루틴 코드 블록이 실제로 어떤 스레드나 스레드풀에서 실행될지를 결정하고, 보내서 실행시키는 역할을 담당합니다.
다음과 같은 방식으로 동작합니다.
- 작업 대기열에 적재 : 실행 요청된 코루틴을 내부 대기열에 저장합니다.
- 스레드 사용 가능 여부 확인 : 자신이 관리하는 스레드풀 내에서 사용 가능한 스레드가 있는지 확인합니다.
- 실행 또는 대기 : 존재 시 해당 스레드로 할당하며, 없으면 대기하고 이후 스레드가 반환되면 다시 실행을 시도합니다.
CoroutineDispatcher의 종류
사용할 수 있는 스레드 또는 스레드풀의 제한 유무에 따라 종류가 구분됩니다.
1. 제한된 디스패처 (Confined Dispatcher)
- 일반적으로는 객체별로 어떤 작업을 처리할지 미리 역할을 부여하고 실행을 요청하는 것이 효율적이므로, 제한을 부여합니다.
- 대부분의 CoroutineDispatcher는 제한된 디스패처로 활용됩니다.
- ex. 입출력 작업(I/O), CPU 연산 작업용
2. 무제한 디스패처 (Unconfined Dispatcher)
- 실행 요청된 코루틴이 이전 코드가 실행되던 스레드에서 계속해서 실행되도록 설정합니다.
- 특정 스레드로 제한되지 않습니다.
2. 제한된 디스패처 생성하기
(1) 하나의 스레드를 관리하는 경우 (단일 스레드)
순차 처리해야 하는 작업(ex. 단일 데이터베이스 접근)을 한 개의 스레드에서 실행할 때 사용합니다.
val singleThreadDispatcher: CoroutineDispatcher = newSingleThreadContext(
name = "SingleThread"
)
(2) 여러 스레드를 관리하는 경우 (멀티 스레드)
병렬 처리할 작업이 여러 개이고, 스레드 개수를 명시적으로 관리하고 싶을 때 사용합니다.
val multiThreadDispatcher: CoroutineDispatcher = newFixedThreadPoolContext(
nThreads = 2,
name = "MultiThread"
)
- 내부적으로 ExecutorService를 생성하는 함수를 통해 스레드풀을 생성합니다.
- 모든 스레드는 데몬 스레드로 생성되어, JVM 종료 시 애플리케이션 종료를 방해하지 않습니다.
- 만들어지는 스레드들은 인자로 받은 name값 뒤에 -1, -2, ... 가 붙습니다. (ex. MultiThread-1)
3. 코루틴에 디스패처 적용하기
(1) launch의 파라미터로 지정하기
launch 함수를 호출해서 만든 코루틴을 특정 CoroutineDispatcher 객체에 실행 요청하기 위해서는 context 인자로 CoroutineDispatcher 객체를 담아서 넘기면 됩니다.
CoroutineDispatcher는 CoroutineContext의 구현체 역할을 할 수 있습니다.
fun main() = runBlocking<Unit> {
val dispatcher = newSingleThreadContext(name = "SingleThread")
launch(context = dispatcher) {
println("[${Thread.currentThread().name}] 실행")
}
}
/*
[SingleThread @coroutine#2] 실행
*/
멀티 스레드의 경우는 다음과 같습니다.
fun main() = runBlocking<Unit> {
val multiThreadDispatcher = newFixedThreadPoolContext(
nThreads = 2,
name = "MultiThread"
)
launch(context = multiThreadDispatcher) {
println("[${Thread.currentThread().name}] 실행")
}
launch(context = multiThreadDispatcher) {
println("[${Thread.currentThread().name}] 실행")
}
}
/*
[MultiThread-1 @coroutine#2] 실행
[MultiThread-2 @coroutine#3] 실행
*/
(2) 부모 코루틴의 디스패처 상속
코루틴은 구조화를 제공해 코루틴 내부에서 새로운 코루틴을 실행할 수 있으며, 별도로 디스패처를 지정하지 않은 자식 코루틴은 부모 코루틴이 사용하던 디스패처를 그대로 상속 받습니다.
여기서 부모 코루틴은 바깥 코루틴, 자식 코루틴은 내부에서 새로 생성되는 코루틴을 말합니다.
fun main() = runBlocking<Unit> {
val multiThreadDispatcher = newFixedThreadPoolContext(
nThreads = 2,
name = "MultiThread"
)
launch(multiThreadDispatcher) { // 부모 Coroutine
println("[${Thread.currentThread().name}] 부모 코루틴 실행")
launch { // 자식 코루틴 실행
println("[${Thread.currentThread().name}] 자식 코루틴 실행")
}
launch { // 자식 코루틴 실행
println("[${Thread.currentThread().name}] 자식 코루틴 실행")
}
}
}
/*
[MultiThread-1 @coroutine#2] 부모 코루틴 실행
[MultiThread-2 @coroutine#3] 자식 코루틴 실행
[MultiThread-1 @coroutine#4] 자식 코루틴 실행
*/
4. 표준 Dispatcher 활용하기
직접 생성한 디스패처 대신, 코루틴 라이브러리가 사전 정의하여 제공하는 Dispatcher를 사용하는 것이 권장됩니다.
직접 생성한 디스패처의 경우 특정 CoroutineDispatcher 객체에서만 사용되는 스레드풀을 만들기 때문에, 스레드 풀 내 스레드 개수의 비효율 가능성과 스레드 생성 비용 등의 이유로 권장되지 않습니다.
- 제한된 디스패처
- Dispatchers.Main : UI가 있는 애플리케이션에서 메인 스레드를 사용하기 위한 용도
- Dispatchers.IO : 네트워크 요청이나 파일 입출력 등의 입출력(I/O) 작업 용도
- Dispatchers.Default : CPU 바운드 작업 (CPU를 많이 사용하는 연산 작업) 용도
- 무제한 디스패처
- Dispatchers.Unconfined : 코루틴을 자신을 호출한 스레드에서 실행하도록 만드는 용도
(1) 스레드의 제한과 공유
스레드 수 제한 방법
Dispatchers.Default를 사용할 때 무거운 연산 처리 시, 모든 스레드를 독점할 수 있는 가능성이 존재합니다.
이때 limitedParallelism 함수를 사용해서, 해당 연산에 할당될 스레드 수를 제한할 수 있습니다.
fun main() = runBlocking<Unit> {
launch(Dispatchers.Default.limitedParallelism(2)){
repeat(10) {
launch {
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
}
}
/*
[DefaultDispatcher-worker-2 @coroutine#1] 코루틴 실행
[DefaultDispatcher-worker-1 @coroutine#2] 코루틴 실행
[DefaultDispatcher-worker-2 @coroutine#3] 코루틴 실행
...
[DefaultDispatcher-worker-1 @coroutine#10] 코루틴 실행
*/
공유 스레드풀
Dispatchers.IO 또는 Dispatchers.Default을 사용하면, 코루틴을 실행시킨 스레드의 이름이 "DefaultDispatcher-worker-n" 임을 확인할 수 있습니다.
코루틴 라이브러리는 스레드의 생성과 관리를 효율적으로 할 수 있도록 애플리케이션 레벨의 공유 스레드풀을 제공하며, 두 DIspatchers는 모두 해당 API를 사용하여 구현되어 동일한 스레드풀을 사용할 수 있습니다.
직접 생성하는 Dispatcher의 경우 전용 스레드풀을 사용하는 것과 다르게 공유 스레드풀을 사용하면 생성 비용을 줄일 수 있습니다.
(2) 무제한 디스패처
무제한 디스패처 (Unconfined Dispatcher)
코루틴이 처음에는 호출된 스레드에서 시작하고, 이후 suspend 함수에서 반환된 시점의 스레드에서 재개됩니다.
호출된 스레드가 무엇이든지 상관없기 때문에 실행 스레드가 제한되지 않습니다.
fun main() = runBlocking<Unit> { // 메인 스레드
launch(Dispatchers.Unconfined) { // Dispatchers.Unconfined를 사용해 실행되는 코루틴
println("launch 코루틴 실행 스레드: ${Thread.currentThread().name}")
}
}
/*
launch 코루틴 실행 스레드: main @coroutine#2
*/
1. 즉시 점유
Dispatchers.Unconfined는 디스패처로 위임 없이 현재 스레드에서 곧바로 실행을 시작합니다.
스레드 스위칭 없이 실행되며, 다른 코루틴이 같은 스레드를 사용할 수 없도록 중단합니다.
fun main() = runBlocking<Unit> {
println("작업1")
launch(Dispatchers.Unconfined) { // Dispatchers.Unconfined를 사용해 실행되는 코루틴
println("작업2")
}
println("작업3")
}
/*
작업1
작업2 (runBlocking 코루틴이 중단됨)
작업3
*/
2. 예측의 어려움
일시 중단 전까지만 자신을 실행시킨 스레드에서 실행되며, 중단 시점 이후의 재개는 코루틴을 재개하는 스레드에서 실행됩니다.
어떤 스레드가 코루틴을 재개시키는지 예측하기 어렵기 때문에, 일반적인 상황에서 사용하면 비동기 작업이 불안정해집니다.
이에 경량 테스트나 디버깅 용도로만 사용이 권장됩니다.
fun main() = runBlocking<Unit> {
launch(Dispatchers.Unconfined) {
println("일시 중단 전 실행 스레드: ${Thread.currentThread().name}")
delay(100L)
println("일시 중단 후 실행 스레드: ${Thread.currentThread().name}")
}
}
/*
일시 중단 전 실행 스레드: main
일시 중단 후 실행 스레드: kotlinx.coroutines.DefaultExecutor
*/
이처럼 코루틴 디스패처는 스레드 자원 관리의 핵심입니다. 직접 생성한 디스패처는 제어력이 높지만 비용이 크므로, 표준 Dispatchers를 활용하여 성능과 안정성을 확보하는 것이 일반적입니다.
'Coroutine' 카테고리의 다른 글
[Coroutine] 6. 구조화된 동시성과 취소 전파 (2) | 2025.05.20 |
---|---|
[Coroutine] 5. CoroutineContext 구성 요소 (0) | 2025.05.19 |
[Coroutine] 4. async와 withContext (0) | 2025.05.19 |
[Coroutine] 3. 코루틴 빌더, 취소, 상태, 그리고 Job (0) | 2025.05.19 |
[Coroutine] 1. 스레드와 코루틴 (0) | 2025.05.19 |