티스토리 뷰
이번에는 코루틴에서 스코프를 열때, 코루틴을 실행할 때! 사용하던 Dispatcher에 대해 알아보고자 한다.
먼저, launch 로 실행할 때 인수로 넣을 수 있는 Dispatcher를 다 넣고 실행해 보았다.
실행했을 때 주석과 같은 이름의 스레드가 동장함을 알 수 있었다.
과연 이 스레드들의 이름은 왜 다른걸까?
그리고 디스패처란 뭘까?
fun main(){
runBlocking {
launch {
logging("1")
//main - 2025-03-16T00:02:03.997624900 : 1
}
launch(Dispatchers.Default) {
logging("2")
//DefaultDispatcher-worker-1 - 2025-03-16T00:02:03.981176900 : 2
}
launch(Dispatchers.Unconfined) {
logging("3")
//main - 2025-03-16T00:02:03.981176900 : 3
}
launch(Dispatchers.IO) {
logging("4")
//DefaultDispatcher-worker-1 - 2025-03-16T00:02:03.997624900 : 4
}
}
}
Dispatcher 란 뭘까?
CoroutineDispatcher클래스를 ctrl를 눌러 보자.
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
이전에 살펴보았던 Context에 속한, Element와 Interceptor 를 상속하고있음을 볼 수 있다.
즉, 코루틴 컨택스트의 요소 중 하나란 것이며,( Element )
코루틴의 실행 흐름을 가로채서 원하는 스레드에서 실행되도록 조정할 수 있다는 것을 의미한다. ( Interceptor )
즉, Dispatcher는 코루틴의 실행 환경을 관리하면서도, 실행되는 스레드를 적절히 조정하는 역할로 이해 할 수 있을 것 같다.
그럼 다시,
Dispatcher 는 종류가 뭐가 있을까?
ctrl을 눌러보자
public actual object Dispatchers {
@JvmStatic
public actual val Default: CoroutineDispatcher = DefaultScheduler
@JvmStatic
public val IO: CoroutineDispatcher = DefaultIoScheduler
...
물론 Main과 Unconfined도 있지만 주로 사용하지 않기에... (공부를 하기위해 찾아 보다가 아닌거같은)
우리가 그냥 launch를 실행했을 때 사용되는 Default와 주로 사용한다는 IO를 살펴보자.
Dispatchers.Default
- DefaultScheduler를 기반으로 하는 기본 디스패처.
- CPU 연산이 많은 작업(예: 데이터 처리, 연산 등)에 적합.
- 코어 개수에 맞춰 적절한 스레드 풀 크기를 가짐.
음.. CPU 연산에 적합하다...
코어 개수에 맞춰 적절한 스레드 풀 크기를 가진다라.. 정말 그럴까? 더 살펴볼까?
internal object DefaultScheduler : SchedulerCoroutineDispatcher(
CORE_POOL_SIZE, MAX_POOL_SIZE,
IDLE_WORKER_KEEP_ALIVE_NS, DEFAULT_SCHEDULER_NAME
) {
@ExperimentalCoroutinesApi
override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
parallelism.checkParallelism()
if (parallelism >= CORE_POOL_SIZE) return this
return super.limitedParallelism(parallelism)
}
...
internal val CORE_POOL_SIZE = systemProp(
"kotlinx.coroutines.scheduler.core.pool.size",
AVAILABLE_PROCESSORS.coerceAtLeast(2),
minValue = CoroutineScheduler.MIN_SUPPORTED_POOL_SIZE
)
그렇다.
가능한 프로세서 수(코어개수)를 기반으로 스레드 풀 크기를 정하는 것을 알 수 있다.
최대한 코어 개수에 맞춰 프로세서 수를 가지는 것은 CPU바운드되는 작업에 적절하게끔 하고자하는 것으로도 보인다.
그렇다면 이제 IO을 보자.
Dispatchers.IO
- 입출력(IO) 작업을 처리하는 디스패처.
- 파일 읽기/쓰기, 네트워크 요청, 데이터베이스 접근 같은 블로킹 작업에 적합.
- 기본적으로 최대 64개의 스레드 또는 CPU 코어 수 중 더 큰 값을 사용.
- Dispatchers.IO.limitedParallelism(n)을 사용하면 특정 개수의 스레드만 사용하도록 제한 가능.
- Dispatchers.IO는 일반적으로 Dispatchers.Default와 스레드를 공유한다.
- withContext(Dispatchers.IO) { ... }를 사용할 때, 기존에 Dispatchers.Default에서 실행 중이라면 스레드 전환이 일어나지 않을 수도 있음.
- 즉, Dispatchers.IO는 Dispatchers.Default와 같은 스레드 풀을 공유하지만, 필요에 따라 추가적인 스레드를 생성하여 블로킹 작업을 처리함.
음.. IO 작업에 적합하며...
최대 64개 스레드..그리고 Default 와 스레드 공유.. ?
좀 더 깊이 살펴보자 .
// Dispatchers.IO
internal object DefaultIoScheduler : ExecutorCoroutineDispatcher(), Executor {
private val default = UnlimitedIoScheduler.limitedParallelism(
systemProp(
IO_PARALLELISM_PROPERTY_NAME,
64.coerceAtLeast(AVAILABLE_PROCESSORS)
)
)
...
흠 그렇구만.. 기본적으로 64스레도 혹은 코어 개수중 더 많은 값을 기준으로
스레드 풀을 유지함을 확인할 수 있다.
그럼 이런생각이 든다.
왜 IO는 최소 64개에 코어 개수에 따라 더크게가 가능한데, (더 관대하게 큰 값을 유지하는데!)
Default는 냉소적으로 코어 개수에 맞추고자 하는걸까?
IO는 왜 더 많은 스레드 풀크기를 갖고 같은 스레드 풀을 사용하는가?
Default 는 CPU 관련 작업을 위한 Dispatcher용도로
CPU 연산(예: 정렬, AI 모델 계산, 암호화, 데이터 가공 등) 에서는 스레드를 너무 많이 늘려봤자 성능이 더 좋아지지 않는다.
그러나 IO는 블로킹 I/O 작업(예: 파일 읽기/쓰기, 네트워크 요청, DB 쿼리 등) 을 처리하기 위한 용도로,
I/O 작업은 CPU를 거의 사용하지 않고, 대부분 I/O 장치 응답을 기다리기에 스레드 개수를 많이 확보해야 높은 동시성을 유지할 수 있기 때문이다.
즉, 그렇기 때문에 Default 와 같은 스레드풀을 사용하면서도 추가적인 스레드가 필요할 때 충분히 블로킹될 수 있는 작업들이라 추가하는 방식을 사용하는 것이다.
그렇다면 왜 Dispatcher가 나뉘며 스레드 개수가 다른지 이해가 가기 시작한다.
코루틴의 실행 환경을 관리하면서도, 실행되는 스레드를 적절히 조정하는 것이,
Dispatcher의 역할이기 때문에 IO와 Defualt를 선택할 수 있게 한것.
따라서, 개발자는 작업의 성격에 맞는 디스패처를 선택함으로써 리소스를 적절하게 관리하고, 시스템의 성능을 최적화할 수 있는 것이다.
더 쉽게 말하면
사실 default랑 IO는 사실 코드에서 큰 차이는 없지만,
IO혹은 CPU작업인지 판단에 따라 스레드를 더 늘리거나 혹은 코어에 맞춰 풀을 둘지 예측해야하기 때문에 ,
이를 Dispatcher로 분리해서 개발자가 맞춰 사용하게끔하여, 스레드 풀을 모르게 효율적으로 사용할 수 있도록 한 것이다.
궁금하다면 두 Dispatcher의 코드를 펼쳐서 보면 된다.
// Dispatchers.DEFUALT
internal object DefaultScheduler : SchedulerCoroutineDispatcher(
CORE_POOL_SIZE, MAX_POOL_SIZE,
IDLE_WORKER_KEEP_ALIVE_NS, DEFAULT_SCHEDULER_NAME
) {
@ExperimentalCoroutinesApi
override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
parallelism.checkParallelism()
if (parallelism >= CORE_POOL_SIZE) return this
return super.limitedParallelism(parallelism)
}
internal fun shutdown() {
super.close()
}
// Overridden incase anyone writes (Dispatchers.Default as ExecutorCoroutineDispatcher).close()
override fun close() {
throw UnsupportedOperationException("Dispatchers.Default cannot be closed")
}
override fun toString(): String = "Dispatchers.Default"
}
// Dispatchers.IO
internal object DefaultIoScheduler : ExecutorCoroutineDispatcher(), Executor {
private val default = UnlimitedIoScheduler.limitedParallelism(
systemProp(
IO_PARALLELISM_PROPERTY_NAME,
64.coerceAtLeast(AVAILABLE_PROCESSORS)
)
)
override val executor: Executor
get() = this
override fun execute(command: java.lang.Runnable) = dispatch(EmptyCoroutineContext, command)
@ExperimentalCoroutinesApi
override fun limitedParallelism(parallelism: Int): CoroutineDispatcher {
// See documentation to Dispatchers.IO for the rationale
return UnlimitedIoScheduler.limitedParallelism(parallelism)
}
override fun dispatch(context: CoroutineContext, block: Runnable) {
default.dispatch(context, block)
}
@InternalCoroutinesApi
override fun dispatchYield(context: CoroutineContext, block: Runnable) {
default.dispatchYield(context, block)
}
override fun close() {
error("Cannot be invoked on Dispatchers.IO")
}
override fun toString(): String = "Dispatchers.IO"
}
참고) ctrl키와 주석, 번역기
'Kotlin' 카테고리의 다른 글
코틀린 코루틴? 원문을 보고 말지 (4) + 비동기 (0) | 2025.03.15 |
---|---|
코틀린 코루틴? 원문을 보고 말지 (3) + 코루틴 구조 (with ctrl) (0) | 2025.03.15 |
코틀린 코루틴? 원문을 보고 말지 (2) + cancel (0) | 2025.03.15 |
코틀린 코루틴? 원문을 보고 말지 (1) + runBlocking, launch (0) | 2025.03.10 |
코틀린 코루틴? 원문을 보고 말지 (preview) (1) | 2025.03.10 |