Dispatcher
따라서 CoroutineDispatcher → 코루틴을 보내는 주체
CoroutineDispatcher는 코루틴을 스레드로 보냄

코루틴 디스패처 객체는 자신에게 실행 요청된 코루틴을 우선 작업 대기열에 적재한 후 사용할 수 있는 스레드가 생기면 스레드로 보내는 방식으로 동작
코루틴 디스패처는 코루틴의 실행을 관리하는 주체
자신에게 실행 요청된 코루틴들을 작업 대기열에 적재하고 자신이 사용할 수 있는 스레드가 새로운 작업을 실행할 수 있는 상태라면 스레드로 코루틴을 보내 실행할 수 있게 만드는 역할
코루틴 디스패처는 코루틴 실행요청이 들어오면 일반적으로 작업 대기열에 적재한 후에 스레드로 보냄
하지만 실행 옵션에 따라 작업 대기열에 적재되지 않고 즉시 실행될 수도 있음
작업 대기열이 없는 코루틴 디스패처 구현체도 있으니 이는 매우 예외적인 경우임
이는 무제한 디스패처에서 별도로 다룸
코루틴 디스패처는 2가지 종류가 있음
일반적으로 코루틴 디스패처는 객체별로 어떤 작업을 처리할지 미리 역할을 부여하고 역할에 맞춰 실행을 요청하는 것이 효율적이기 때문에 대부분의 디스패처는 제한된 디스패처임
입출력 작업 → 입출력 작업용 콜틴 디스패처 객체에 실행을 요청
CPU 연산 작업 → CPU 연산 작업용 코루틴 디스패처 객체에 실행을 요청
무제한 디스패처라고 해서 코루틴이 아무 스레드에서나 실행되는 것은 아님
코루틴 라이브러리에서 제공하는 제한된 디스패처 생성 함수를 다룸
단일 스레드 디스패처 → 사용할 수 있는 스레드가 하나
newSingleThreadContext() 함수를 통해 만들 수 있음
val dispatcher : CoroutineDispatcher = newSingleThreadContext(name = "SingleThread")
이 함수를 통해 만들어진 디스패처는 작업 대기열과 스레드 하나로 구성된 스레드풀을 사용할 수 있음
이 스레드의 이름은 인자로 넘긴 “SingleThread”가 됨
코루틴 라이브러리의 newFixedThreadPoolContext() 함수를 통해 만들 수 있음
val multiThreadDiapatcher: CoroutineDispatcher = newFixedThreadPoolContext(
nThreads = 2,
name = "MultiThread"
)
이 함수를 통해 만들어진 디스패처는 작업 대기열과 MultiThread-1, MultiThread-2라는 이름을 가진 스레드 2개가 존재함
이렇게 만들어진 디스패처는 newSingleThreadContext()로 만들어진 디스패처와 모습이 매우 흡사함
newSingleThreadContext()가 내부적으로 newFixedThreadPoolContext()를 사용하도록 구현되어 있기 때문임
public fun newSingleThreadContext(name: String):
ClosableCoroutineDispatcher =
newFixedThreadPoolContext(1, name)
단일 스레드 디스패처 사용해 코루틴 실행하기
launch함수를 사용해 만든 코루틴을 특정 코루틴 디스패처 객체에 실행 요청하기 위해서는 launch 함수의 context 인자로 코루틴 디스패처 객체를 넘기면 됨
fun main() = runBlocking<Unit> {
val dispathcer = newSingleThreadContext(name = "SingleThread")
launch(context = dispatcher) {
println("[${Thread.currentThread().name}] 실행")
}
}
// [SingleThread @coroutine#2] 실행
새로운 스레드의 이름을 SingleThread로 설정했으므로 launch 함수를 통해 생성된 코루틴이 실행되는 스레드가 SingleThread로 나옴
멀티 스레드 디스패처를 사용해 코루틴 실행하기
단일 스레드 디스패처를 사용하는 방식과 동일
launch 함수의 context 인자로 multiThreadDispatcher를 넘기면 됨
fun main() = runBlocking<Unit> {
val dispathcer = newFixedThreadPoolContext(
nThreads = 2,
name = "SingleThread"
)
launch(context = dispatcher) {
println("[${Thread.currentThread().name}] 실행")
}
launch(context = dispatcher) {
println("[${Thread.currentThread().name}] 실행")
}
}
단일 스레드 코루틴 디스패처와 마찬가지로 실행을 요청받은 코루틴을 작업 대기열에 적재하고 작업 가능한 스레드에 코루틴을 보내 실행
다음 작업도 마찬가지로 작업 대기열에 적재 한 후 사용 가능한 스레드에 코루틴을 보내 실행함
이 때 실행 환경에 따라 각 코루틴이 다른 속도로 처리될 수 있기 때문에 출력되는 스레드 이름이 같을 수 있음
코루틴은 구조화를 제공해 콜틴 내부에서 새로운 코루틴을 실행할 수 있음
이 때 바깥쪽 코루틴을 부모 코루틴, 내부에서 실행되는 코루틴을 자식 코루틴이라고 함
구조화는 코루틴을 계층 관계로 만드는 것 뿐 아니라 부모 코루틴의 실행 환경을 자식 코루틴에 전달하는 데도 사용됨
만약 자식 코루틴에 코루틴 디스패처 객체가 설정되지 않았으면 부모 코루틴의 코루틴 디스패처 객체를 사용함
fun main() = runBlocking<Unit> {
val dispathcer = newFixedThreadPoolContext(
nThreads = 2,
name = "SingleThread"
)
launch(context = dispatcher) { // 부모
println("[${Thread.currentThread().name}] 부모 코루틴 실행")
launch(context = dispatcher) { // 자식
println("[${Thread.currentThread().name}] 자식 코루틴 실행")
}
launch(context = dispatcher) { // 자식
println("[${Thread.currentThread().name}] 자식 코루틴 실행")
}
}
}
/*
[MultiThread-1 @coroutine#2] 부모 코루틴 실행
[MultiThread-2 @coroutine#3] 자식 코루틴 실행
[MultiThread-1 @coroutine#4] 자식 코루틴 실행
*/
가장 상단의 launch 함수로 생성되는 코루틴이 부모 코루틴이 됨
이 부모 코루틴은 전용 스레드가 2개인 코루틴 디스패처를 사용
부모 코루틴 launch 함수의 람다식 내부에서 2개의 launch 함수를 호출해2개의 코루틴을 추가로 생성하는데 이 때 생성되는 코루틴이 자식 코루틴임
이 자식 코루틴들에는 별도의 코루틴 디스패처가 설정되어 있지 않기 때문에 부모의 코루틴 디스패처를 사용함
부모 코루틴과 자식 코루틴이 동일한 코루틴 디스패처 객체를 사용하므로 스레드를 공유하는 것을 볼 수 있음
특정 코루틴 디스패처에서 여러 작업을 실행해야 한다면 부모 코루틴에 코루틴 디스패처 객체를 설정하고 그 아래 자식 코루틴을 여러개 생성하면 됨
멀티 스레드 프로그래밍에서 가장 많이 사용되는 작업 → I/O 작업
I/O 작업 여러 개를 동시에 수행하므로 이런 요청을 동시에 수행하기 위해서는 많은 스레드가 필요함
코루틴 라이브러리에서는 입출력 작업을 위해 미리 정의된 Dispatchers.IO를 제공함
Dispatchers.IO가 최대로 사용할 수 있는 스레드의 수 → JVM에서 사용할 수 있는 프로세서 수와 64 중 큰 값으로 설정되어 있음
Dispatchers.IO는 싱글톤 인스턴스이므로 launch 함수의 인자로 곧바로 넘겨줄 수 있음
fun main() = runBlocking<Unit> {
launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
// [DefaultDispathcer-worker-1 @coroutine#2] 코루틴 실행
위 코드의 결과를 보면 코루틴이 실행된 스레드의 이름 앞에 DefaultDispatcher-worker가 붙은 것을 알 수 있음
DefaultDispatcher-worker가 붙은 스레드는 코루틴 라이브러리에서 제공하는 공유 스레드풀에 속한 스레드임
Dispatchers.IO는 공유 스레드풀의 스레드를 사용할 수 있도록 구현됐음
공유 스레드풀에 대해서는 추후 더 자세하게 다룰 예정
CPU 바운드 작업 → CPU 연산이 필요한 작업
ex) 대용량 데이터 처리
Dispatchers.Default → CPU 바운드 작업이 필요할 때 사용하는 CoroutineDispatcher
Dispatchers.Default도 그 자체로 싱글턴 인스턴스이므로 간단하게 호출할 수 있음
fun main() = runBlocking<Unit> {
launch(Dispatchers.Default) {
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
// [DefaultDispatcher-worker-1 @coroutine#2] 코루틴 실행
두 작업의 중요한 차이 → 실행됐을 때 스레드를 지속적으로 사용하는지 여부
일반적으로 입출력 작업은 작업(네트워크 요청, DB 조회 요청 등)을 실행한 후 결과를 반환받을 때까지 스레드를 사용하지 않음
CPU 바운드 작업은 작업을 하는 동안 스레드를 지속적으로 사용
이 차이로 인해 CPU 바운드 작업은 스레드 기반 작업을 사용했을 때와 코루틴을 사용했을 때 효율성에 차이가 발생함
입출력 작업에 코루틴을 사용 → 작업 실행 후 스레드가 대기하는 동안 해당 스레드에서 다른 작업을 동시에 실행할 수 있어 효츌적
CPU 바운드 작업에 코루틴 사용 → 스레드가 지속적으로 사용되기 때문에 스레드 기반 작업을 사용해 실행했을 때와 처리 속도에 차이가 거의 없음
| I/O 작업 | CPU 바운드 작업 | |
|---|---|---|
| 스레드 기반 작업 사용 시 | 느림 | 비슷 |
| 코루틴 사용 시 | 빠름 |
Dispatchers.Default를 사용해 무겁고 오래 걸리는 작업을 처리하면 특정 작업을 위해 Dispatchers.Default의 모든 스레드가 사용될 수 있으며, 특정 연산을 진행하는 동안 Dispatchers.Defualt를 사용하는 다른 작업이 실행되지 못할 수 있음
이를 방지하기 위해 코루틴 라이브러리는 Dispatchers.Default의 일부 스레드만 사용해 특정 연산을 실행할 수 있도록 하는 limitedParallelism 함수를 지원함
fun main() = runBlocking<Unit> {
launch(Dispatchers.Default.limitedParallelism(2)) {
repeat(10) {
launch {
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
}
}
/*
[DefaultDispatcher-worker-2 @coroutine#3] 코루틴 실행
[DefaultDispatcher-worker-1 @coroutine#4] 코루틴 실행
[DefaultDispatcher-worker-2 @coroutine#5] 코루틴 실행
[DefaultDispatcher-worker-1 @coroutine#6] 코루틴 실행
.
.
.
[DefaultDispatcher-worker-2 @coroutine#11] 코루틴 실행
[DefaultDispatcher-worker-1 @coroutine#12] 코루틴 실행
*/
Dispatchers.IO와 Dispatchers.Default를 사용한 코드에서 발견할 수 있는 공통점 → 코루틴을 실행한 스레드의 이름에 DefaultDispatcher-worker가 표시됨
Dispatchers.IO와 Dispatchers.Default는 코루틴 라이브러리의 공유 스레드풀을 사용한다는 것을 의미
코루틴 라이브러리는 스레드의 생성과 관리를 효율적으로 할 수 있도록 애플리케이션 레벨의 공유 스레드풀을 제공
이 공유 스레드풀에는 스레드를 무한으로 생성할 수 있음
코루틴 라이브러리는 공유 스레드풀에 스레드를 생성하고 사용할 수 있도록 하는 API를 제공
Dispatchers.IO와 Dispatchers.Default는 모두 위에서 언급한 API를 사용해 구현됐기 때문에 공유 스레드풀을 사용함
다만 공유 스레드풀에서 Dispatchers.IO와 Dispatchers.Default가 사용하는 스레드는 구분됨

newFixedThreadPoolContext 함수로 만들어지는 디스패처는 자신만 사용할 수 있는 전용 스레드풀을 생성하는 것과 다르게 Dispatchers.IO와 Dispatchers.Default는 공유 스레드풀의 스레드를 사용함
Dispatchers.IO의 limitedParallelism
Dispatchers.Default에 limitedParallelism 함수를 사용하면 스레드풀의 스레드 중 일부만 사용하도록 제한할 수 있음
하지만 Dispatchers.IO의 limitedParallelism은 조금 다르게 동작함
Dispatchers.IO의 limitedParallelism은 공유 스레드풀의 스레드로 구성된 새로운 스레드풀을 만들어내며, 스레드의 수를 제한 없이 만들 수 있음
fun main() = runBlocking<Unit> { launch(Dispatchers.IO.limitedParallelism(100)) { reapeat(200) { launch { Thread.sleep(1000L) println("[${Thread.currentThread().name}] 코루틴 실행") } } } } /* [DefaultDispatcher-worker-60] 코투린 실행 [DefaultDispatcher-worker-74] 코투린 실행 [DefaultDispatcher-worker-68] 코투린 실행 . . . [DefaultDispatcher-worker-85] 코투린 실행 [DefaultDispatcher-worker-49] 코투린 실행 [DefaultDispatcher-worker-98] 코투린 실행 */
- Dispatchers.IO.limitedParallelism(100)를 통해 공유 스레드풀 상에서 100개의 스레드로 구성된 스레드풀을 만들어냄
Dispatchers.IO의 limitedParallelism함수는 공유 스레드풀 상에서 Dispatchers.IO나 Dispatchers.Default와 관계 없는 스레드로 구성된 스레드풀을 만들어냄
특정 작업이 다른 작업에 영향을 받지 않아야 해, 별도 스레드 풀에서 실행되는 것이 필요할 때 사용
다만 공유 스레드풀에서 새로운 스레드를 만들어내는 작업이라 비용이 비싼 작업이므로 남용하면 안됨
UI가 있는 애플리케이션에서 메인 스레드를 사용하기 위해 사용되는 특별한 CoroutineDispatcher 객체
코루틴 라이브러리 추가만으론 사용할 수 없고 별도의 라이브러리(ex. Android)를 추가해야 사용할 수 있음
UI 스레드(Main Thread)를 사용하는 라이브러리가 없는 상태로 Diapatchers.Main을 호출하면 IllegalStateException이 발생함
Dispatchers.Main의 구현체를 제공하는 라이브러리가 없어 생기는 오류임