[Kotlin] Coroutine Dispatcher

유정현·2024년 5월 8일
1
fun postLogin(data: RequestLoginDto) {
        viewModelScope.launch(Dispatchers.IO) {
            runCatching {
                loginService.login(data)
            }
            ...
        }
    }

솝트 과제를 하면서 Dispatcher에 대해 알게 되었다. Dispatcher이 무엇일까?

Dispatcher?

dispatch는 보내는 의미를 가지고 있다.

모든 작업은 Thread 위에서 실행된다. 이때 Dispatcher는 Coroutine을 어느 Thread에 보낼지 정하는 객체이다. Coroutine을 만들어 CoroutineDispatcher로 실행을 요청하면, CoroutineDispatcher는 사용할 수 있는 Thread Pool의 Thread 중 하나에 Coroutine을 보낸다.

종류

Default

  • CPU를 많이 사용하는 작업을 실행하기 위한 디스패처
  • CPU 개수만큼 Thread를 생성해 작업
  • 무거운 연산 작업에 최적화
  • 정렬 작업이나 JSON Parsing 작업 등을 위해 사용
  • JVM에서 제공되는 공용 백그라운드 Thread Pool을 사용

Main

  • UI 작업을 하기 위해 사용

IO

  • 네트워크, DB 작업, 디스크 작업 등에 최적화
  • Thread를 Block할 필요가 있을 때 사용
  • 기본적으로 최대 64개의 Thread를 생성할 수 있음

Unconfined

  • 아무런 context를 지정하지 않겠다는 의미로 사용
  • 해당 코루틴을 호출한 Thread에서 실행
  • 호출한 context를 기본으로 사용하는데 중단 후 다시 실행될 때 context가 바뀌면 바뀐 context를 따라가는 특이한 Dispatcher

사용 방법

launch(Dispatchers.Main) {
	// Coroutine이 실행할 작업
}
fun main() {
    runBlocking {
        launch {
            println("main: 쓰레드는 ${Thread.currentThread().name}")
        }
        
        launch(Dispatchers.Unconfined) {
            println("Unconfined: 쓰레드는 ${Thread.currentThread().name}")
        }

        launch(Dispatchers.IO) {
            println("IO: 쓰레드는 ${Thread.currentThread().name}")
        }
        
        launch(Dispatchers.Default) {
            println("Default: 쓰레드는 ${Thread.currentThread().name}")
        }
    }
}

위 코드를 실행하면 다음과 같은 결과가 나온다

정리하자면, Default와 IO는 Worker Thread에서 실행되었고, Main과 Unconfined는 Main Thread에서 실행되었다.

1) Default vs IO

두 Dispatcher의 차이를 알아보자
위에서 DefaultCPU 개수만큼 Thread를 생성해 작업한다고 했다. 즉, CPU 코어 개수보다 더 많은 Thread를 사용하게 되면 오버헤드가 발생하게 된다.

오버헤드?
작업1과 작업2를 번갈아가며 수행한다고 하자. 작업1을 하다가 기록을 하고, 작업 2를 하다가 다시 작업 1을 할 때 다시 불러오는 작업이 필요하다. 이때 작업 전환을 문맥 교환(Context Switching)이라고 하고 이때 낭비되는 시간이 오버헤드이다.

그래서 Default는 최대 개수를 CPU 개수만큼 제한을 둔다. 즉, 한 명에게 하나의 작업만 시킨다.

IO는 Thread가 Blocking 되기 때문에 더 많은 Thread를 사용하기 위해 대기시간이 있는 작업들을 수행한다.

2) Unconfined Dispatcher

Unconfined는 해당 코루틴을 호출한 Thread에서 실행하게 하는데, 사실은 이 Thread에서 실행을 시작해도 코루틴의 첫 번째 중단점까지만 실행된다. 다시 재개될 때에는 Suspend Function이 호출된 Thread에서 재개된다. Unconfined Dispatcher는 CPU time을 소비하지 않고 UI와 같은 공유 데이터를 update하지 않는 등의 경우에 사용하면 적절하다.

launch(Dispatchers.Unconfined) {
            println("Unconfined             : 나는 ${Thread.currentThread().name} 에서 돈다")
            delay(500)
            println("Unconfined             : 딜레이 이후에는 ${Thread.currentThread().name} 에서 돈다")
        }
        launch {
            println("main runBlocking       : 나는 ${Thread.currentThread().name} 에서 돈다")
            delay(1000)
            println("main runBlocking       : 딜레이 이후에는 ${Thread.currentThread().name} 에서 돈다")
        }

runBlocking으로부터 상속받은 context로 동작하는 코루틴은 main Thread에서 재개되었고, Unconfined는 DefaultExecutor Thread에서 재개되었음을 확인할 수 있다. 즉 Unconfined Dispatcher는 해당 코루틴을 호출한 Thread에서 실행을 하게하고 중단 이후에는 그저 불러주는 Thread에서 실행된다는 것이다. 여기에서는 DefaultExecutor에서 delay()가 호출된 것이다.

공식 문서에 의하면 Unconfined Dispatcher는 코루틴의 일부 작업이 즉시 수행되어야해서 Thread 전환을 하고자 코루틴이 잠시 dispatch되어 동작이 나중에 수행되는 등 side effect가 발생할 수 있어 위험하다고 한다. 따라서 일반적인 경우에는 Unconfined Dispatcher를 사용하지 않는 것이 좋다


[참고자료]
https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html#unconfined-vs-confined-dispatcher

0개의 댓글

관련 채용 정보