티스토리 뷰

이번에는 코루틴에서 스코프를 열때, 코루틴을 실행할 때! 사용하던 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키와 주석, 번역기

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함