Primary/Kotlin

[Kotlin] Dispatcher

해스끼 2022. 11. 8. 19:35

Kotlin Coroutines에서 Dispatcher는 코루틴이 실행되는 스레드를 결정한다. 특정한 스레드를 지정할 수도 있고, thread pool에 넘겨줄 수도 있고, 아예 정하지 않을 수도 있다.

 

``launch``, ``async``, ``withContext`` 등의 모든 코루틴 빌드 함수는 ``CoroutineContext`` 타입의 매개변수를 받는다. 따라서 필요하다면 dispatcher를 매개변수로 넘겨줄 수 있다.

launch(Dispatchers.Default) { 
    // do something
}

이 글에서는 Kotlin에서 사용할 수 있는 dispatcher에 대해 자세히 알아보겠다.

Dispatchers.Main

메인 스레드, 즉 UI 스레드를 의미하는 dispatcher이다. JVM에서 ``Dispatchers.Main``은 메인 스레드만을 사용하기 때문에, 여기서 실행한 코루틴은 UI 작업을 block할 수도 있다. JS와 Native에서는 ``Dispatchers.Default``와 같기 때문에 UI를 block하는 일은 없을 것이다.

 

메인 스레드에서 작업을 최대한 빨리 실행하는 ``Dispatchers.Main.immediate``도 있다. 현재 dispatcher가 ``Main``이라면 코드를 즉시 실행하고, ``Main``이 아니라면 ``Main``으로 최대한 빨리 thread switch하여 코드를 실행한다. JVM에서만 사용할 수 있다.

suspend fun updateText(newText: String) = withContext(Dispatchers.Main.immediate) {
    uiElement.text = newText
}

Dispatchers.Default

이름에서 알 수 있듯이 기본 dispatcher이다. ``launch`` 등에서  dispatcher가 지정되지 않았고, 상속받을 부모 context가 없다면 ``Dispatchers.Default``가 사용된다. 대부분에 코루틴에 적용하기 좋으며, 특히 CPU를 많이 사용하는 작업에 유용하다.

 

JVM 내부에서는 shared thread pool을 사용한다. Shared thread pool에는 기본적으로 CPU 코어 수만큼 스레드가 만들어져 있지만, 싱글 코어 머신에서도 최소 2개의 스레드가 만들어진다.

Dispatchers.IO

필요할 때 만들어지는(on-demand) 스레드 풀을 사용한다. 파일 입출력이나 네트워크 작업 등 IO 작업을 수행하는 코루틴에 적합하다.

 

On-demand pool의 스레드는 필요할 때 만들어지고, 작업이 완료될 때 같이 종료된다. 그래서 작업이 수행되지 않을 때에는 매우 적은 수의 스레드만이 존재하지만, 작업이 매우 많이 실행될 때에는 스레드를 필요한 만큼 만들 수 있다.

 

``Dispatchers.IO.limitedParallelism()`` 함수를 통해 dispatcher가 만들 수 있는 스레드의 수를 제한할 수 있다. 참고로  ``Dispatchers.IO``는 최대 ``max(64, CPU 코어 수)``개의 스레드를 가질 수 있다. 이 개념을 Kotlin 문서에서는 Elasticity for limited parallelism이라고 표현했다.

// myDispatcher는 최대 100개의 스레드를 만들 수 있다.
val myDispatcher = Dispatchers.IO.limitedParallelism(100)

사실 ``Dispatchers.IO``는 ``Dispatchers.Default``와 스레드를 공유할 수 있다. 그래서 ``Default`` 안에서 ``withContext(Dispatcheres.IO) { ... }``를 실행하더라도 thread switching이 일어나지 않는다. 불필요한 오버헤드를 줄이기 위한 방법이다.

 

그래서 IO dispatcher로 작업을 실행하다 보면 Main과 스레드를 공유하면서 64개 이상의 스레드가 만들어질 수도 있다. 그렇다고 해서 만들어진 스레드가 전부 사용되는 건 아니다.

Dispatchers.Unconfined

특정 스레드를 지정하지 않는 dispatcher이다. 처음에는 현재 context에서 코루틴을 계속 실행하고, 코루틴이 멈췄다 다시 실행될 때에는 다시 실행되기 직전에 다른 코루틴이 사용하던 스레드에서 실행된다. 일반적인 상황에서 사용할 일은 거의 없을 것이다. Kotlin 공식 문서에서도 The Unconfined dispatcher should not normally be used in code라고 언급하고 있다.

 

코루틴을 아무 스레드에서나 시작할 수 있게 하면서 멈췄다 다시 동작할 때 특정 종류의 스레드에서만 실행하고 싶다면 코루틴 빌더에 ``CoroutineStart.UNDISPATCHED``를 매개변수로 넘겨주면 된다.

launch(start = CoroutineStart.UNDISPATCHED) {
    // do something
}

newSingleThreadContext(), newFixedPoolContext()

하나 또는 고정된 개수의 스레드 pool을 사용하는 ``CoroutineDispatcher``를 반환한다. 그런데 이 함수들은 delicate API로 지정되어 있어서, 함수를 정확히 이해하고 사용하라는 경고가 뜬다. 

C++에서 동적으로 할당된 메모리를 관리하지 않으면 누수가 날 수 있는 상황과 비슷한 듯하다. Thread pool을 명시적으로 close하지 않으면 CPU와 메모리를 계속 점유할 수도 있다고.

 

웬만하면 ``Default``랑 ``IO`` 쓰자.

java.util.concurrent.Executor

Java를 사용할 때 자주 썼던 ``Executor``를 dispatcher로 변환할 수도 있다. ``Executor``는 주어진 ``Runnable``을 실행하는 추상적인 객체로, Java에서 스레드에 신경쓰지 않고 작업을 실행할 수 있게 해 준다. 

 

``Executor.asCoroutineDispatcher()``를 실행하면 ``Executor``를 ``CoroutineDispatcher``로 형변환할 수 있다.

private suspend fun test() = viewModelScope.launch(Executor { /* do something */ }.asCoroutineDispatcher()) {
        /* do another thing */
}

쓸 일이 있나 싶지만, 공부해서 나쁠 건 없지.