디스패처 : 사람이나 차량, 특히 긴급 차량을 필요한 곳에 보내는 것을 담당하는 사람
코루틴이 실행될 스레드를 결정하는 기능을 제공하는 디스패처에 대해서 알아보자!
suspend fun main() = coroutineScope {
repeat(1000) {
launch { // 또는 launch(Dispatchers.Default)
// 바쁘게 만들기 위해 실행
List(1000) { Random.nextLong() }.maxOrNull()
val threadName = Thread.currentThread().name
println("Running on thread: SthreadName")
}
}
}
limitedParallelism을 사용하면 디스패처가 같은 스레드 풀을 사용하지만 같은 시간에 특정 수 이상의 스레드를 사용하지 못하도록 제한할 수 있음
private val dispatcher = Dispatchers.Default.limitedParallelism(5)
Dispatchers.Main을 사용class SomeTest {
private val dispatcher = Executors
.newSingleThreadExecutor()
.asCoroutineDispatcher()
@Before
fun setup() {
Dispatchers.setMain(dispatcher)
}
@After
fun tearDown() {
// 메인 디스패처를 원래의 Main 디스패처로 되돌림
Dispatchers.resetMain()
dispatcher.close()
}
@Test
fun testSomeUI() = runBlocking {
launch(Dispatchers.Main) {
// ...
}
}
}
limitedParallelism는 독립적인 스레드 풀을 가진 새로운 디스패처를 만듦
100개의 코루틴이 각각 스레드를 1초씩 블로킹하는 경우,
Dispatchers.IO에서 실행하면 2초가 걸림
limitedParallelism으로 100개의 스레드를 사용하는 Dispatchers.IO에서 실행하면 1초가 걸림

limitedParallelism을 잘 활용하는 방법
asCoroutineDispatcher를 사용하여 디스패처로 변형할 수 있음ExecutorService.asCoroutineDispatcher()로 만들어진 디스패처는 close 함수로 닫혀야함// 10,000개의 코루틴이 i를 1씩 증가시켰지만, 실제로는 더 작은 값을 갖게 됨
var i = 0
suspend fun main(): Unit = coroutineScope {
repeat(10_000) {
launch(Dispatchers.IO) {
i++
}
}
delay(1000)
println(i) // ~9930
}
val dispatcher = Executors.newSingleThreadExecutor()
.asCoroutineDispatcher()
var i = 0
suspend fun main(): Unit = coroutineScope {
val dispatcher = Dispatchers.Default
.limitedParallelism(1)
repeat(10_000) {
launch(dispatcher) {
i++
}
}
delay(1_000)
println(i) // 10_000
}
대신 단 하나의 스레드만 가지고 있기 때문에 스레드가 블로킹되면 작업이 순차적으로 처리되는 단점이 있음
JVM 플랫폼은 “프로젝트 룸”을 사용하여 일반 스레드보다 가벼운 가상 스레드를 도입했음
각각의 코루틴이 1초 동안 블로킹되는 100,000 개의 코루틴을 시작하면,
아직 실제로 사용하기엔 어렵지만, Dispatchers.IO를 대체할 수 있는 경쟁자가 될 수 있음
Dispatchers.Unconfined의 특징suspend fun main(): Unit =
withContext(newSingleThreadContext("Thread1")) {
var continuation: Continuation<Unit>? = null
launch(newSingleThreadContext("Thread2")) {
delay(1000)
continuation?.resume(Unit)
}
launch(Dispatchers.Unconfined) {
println(Thread.currentThread().name) // Thread1
suspendCancellableCoroutine<Unit> {
continuation = it
}
println(Thread.currentThread().name) // Thread2
delay(1000)
println(Thread.currentThread().name)
// kotlinx.coroutines.DefaultExecutor
// (delay가 사용한 스레드)
}
}
kotlinx-coroutines-test 의 runTest 를 사용하면 별도 설정 없이도 동일한 효과를 얻을 수 있음withContext를 통해 코루틴 디스패처를 변경하면, 중단 후 큐에서 재개되는 비용이 발생함Dispatchers.Main.immediate를 사용하면, 메인 스레드에서 즉시 실행되어 추가적인 배정 비용이나 지연을 피할 수 있음ContinuationInterceptor라는 코루틴 컨텍스트는 코루틴이 중단되었을 때 interceptContinuation 메서드로 컨디뉴에이션 객체를 수정하고 포장함releasInterceptedContinuation 메서드는 컨티뉴에이션이 종료되었을 때 호출됨public interface ContinuationInterceptor :
CoroutineContext.Element {
companion object Key :
CoroutineContext.Key<ContinuationInterceptor>
fun <T> interceptContinuation(
continuation: Continuation<T>
): Continuation<T>
fun releasInterceptedContinuation(
continuation: Continuation<*>
) { ... }
}
interceptContinuation을 통해 DispatchedContinuation을 래핑하여 컨티뉴에이션을 제어함100개의 독립적인 코루틴이 1초동안 중단하는 작업, 1초 동안 블로킹하는 작업, CPU 집약적인 연산, 메모리 집약적인 연산을 수행하는데 얼마나 시간이 걸리는지 비교 (단위: ms)
| 중단 | 블로킹 | CPU 집약적인 연산 | 메모리 집약적인 연산 | |
|---|---|---|---|---|
| 싱글스레드 | 1,002 | 100,003 | 39,103 | 94,358 |
| 디폴트 디스패처(스레드 8개) | 1,002 | 13,003 | 8,473 | 21,461 |
| IO 디스패처(스레드 64개) | 1,002 | 2,003 | 9,893 | 20,776 |
| 스레드 100개 | 1,002 | 1,003 | 16,379 | 21,004 |
// 테스트 함수
suspend fun suspending(order: Order): Coffee {
delay(1000)
return Coffee(order)
}
fun blocking(order: Order): Coffee {
Thread.sleep(1000)
return Coffee(order)
}
fun cpu(order: Order): Coffee {
var i = Int.MAX_VALUE
while(i > 0) {
i -= if (i % 2 == 0 ) 1 else 2
}
return Coffee(order.copy(customer = order.customer + i))
}
fun memory(order: Order): Coffee {
val list = List(1_000) { it }
val list2 = List(1_000) { list }
val list3 = List(1_000) { list2 }
return Coffee(
order.copy(
customer = order.customer + list3.hashCode()
)
)
}