Kotlin Coroutines

[Kotlin Coroutines] 디스패처

easyhz 2024. 5. 27. 16:25

기본 디스패처

디스패처를 설정하지 않으면 기본적으로 설정되는 디스패처는 CPU 집약적인 연산을 수행하도록 설계된 `Dispatchers.Default`이다.

이 디스패처는 코드가 실행되는 컴퓨터의 CPU 개수와 동일한 수(최소 두 개 이상)의 스레드 풀을 가지고 있다. CPU 집약적인 연산을 수행하며 블로킹이 일어나지 않는 환경 같이 스레드를 효율적으로 사용하고 있다고 가정하면 이론적으로는 최적의 스레드 수라고 할 수 있다.

다음 코드를 통해 디스패처를 확인할 수 있다.

suspend fun main(): Unit = coroutineScope {
   repeat(1_000) {
      launch(Dispatchers.Default) {
         List(1000) {
            Random.nextLong()
         }.maxOrNull()

         val threadName = Thread.currentThread().name
         println("나는 지금 : $threadName")
      }
   }
}

 

기본 디스패처를 제한하기

비용이 많이 드는 작업이 `Dispatchers.Default`의 스레드를 다 써버려서 같은 디스패처를 사용하는 다른 코루틴이 실행될 기회를 제한하고 있다고 의심하는 상황일 때, `limitedParallelism`을 사용해 제한할 수 있다. 다음과 같이 사용하면 디스패처가 같은 스레드 풀을 사용하지만 같은 시간에 특정 수 이상의 스레드를 사용하지 못하도록 제한할 수 있다.

val dispatcher = Dispatchers.Default.limitedParallelism(5)

 

 

메인 디스패처

안드로이드에서는 메인 스레드 (UI 스레드)가 가장 중요하다. 메인 스레드는 UI와 상호작용하는 데 사용하는 유일한 스레드이며, 메인 스레드가 블로킹되면 전체 애플리케이션이 멈춰버린다. 메인 스레드에서 코루틴을 실행하려면 `Dispatchers.Main`을 사용하면 된다.

안드로이드에서는 기본 디스패처로 메인 디스패처를 주로 사용한다. 

 

IO 디스패처

`Dispatchers.IO`는 파일을 읽고 쓰는 경우, 안드로이드의 shared preference를 사용하는 경우, 블로킹 함수를 호출하는 경우처럼 I/O 연산으로 스레드를 블로킹할 때 사용하기 위해 설계되었다. 

`Dispatchers.IO`는 같은 시간에 50개가 넘는 스레드(정확히는 `max(64, CPU 코어수)` )를 사용할 수 있도록 만들어졌기 때문에 다음 코드는 1초밖에 걸리지 않는다.

suspend fun main() {
   val time = measureTimeMillis {
      coroutineScope {
         repeat(50) {
            launch(Dispatchers.IO) {
               Thread.sleep(1000)
            }
         }
      }
   }
   println("걸린 시간 $time")
}

 

 

`Dispatchers.Default` 와 `Dispatchers.IO`는 같은 스레드 풀을 공유한다.

 

커스텀 스레드 풀을 사용하는 IO 디스패처

`limitedParallelism`함수는 독립적인 스레드 풀을 가진 새로운 디스패처를 만든다. 이렇게 만들어진 풀은 우리가 원하는 만큼 많은 수의 스레드 수를 설정할 수 있으므로 스레드 수가 64개로 제한되지 않는다.

스레드의 한도가 `Dispatchers.IO`를 비롯한 다른 디스패처와 무관하기 때문에 한 서비스가 다른 서비스를 블로킹하는 경우는 없다.

따라서 `limitedParallelism`을 적절히 활용하는 것이 좋겠다.

 

프로젝트 룸의 가상 스레드 사용하기

`JVM` 플랫폼은 프로젝트 룸이라는 새로운 기술을 발표했다.

일반적인 스레드보다 훨씬 가벼운 가상 스레드를 도입했다. 일반적인 스레드를 블로킹하는 것보다 가상 스레드를 블로킹하는 것이 비용이 훨씬 적게 든다.

스레드를 블로킹할 수밖에 없는 `Dispatchers.IO` 대신 가상 스레드를 사용할 수 있다.

디스패처 룸에서 작업을 수행한 결과 스레드에 대한 블로킹이 있을 때 기존의 스레드 보다 더 빠른 성능과 시간을 보인다.

val LoomDispatcher = Executors
	.newVirtualThreadPerExecutor()
    .asCoroutineDispatcher()

launch(LoomDispatcher) {
    Thread.Sleep(1000)
}

 

제한받지 않는 디스패처

`Dispatchers.Unconfined`는 스레드를 바꾸지 않는다.

제한받지 않는 디스패처가 시작되면 시작한 스레드에서 실행이 되고, 재개되었을 때는 재개한 스레드에서 실행된다.

단위 테스트할 때 유용하며, 스레드 스위칭을 일으키지 않아 비용이 가장 저렴하다.

 

안드로이드의 메인 스레드에서 실행하면, 전체 애플리케이션이 블로킹된다고 한다.

 

 

 

메인 디스패처로 즉시 옮기기

코루틴을 배정하는 것에도 비용이 든다.

`withContext`가 호출되면 코루틴은 중단되고 큐에서 기다리다가 재개된다. 

`Dispatchers.Main.immediate` 를 사용하면 메인 스레드에서 함수를 호출하면 스레드 배정 없이 즉시 실행된다.

호출하고자 하는 함수가 이미 메인 디스패처에서 호출이 되었다면 `withContext`의 인자로 `Dispatchers.Main.immediate`를 사용하는 것이 좋다.

 

컨티뉴에이션 인터셉터

`ContinuationInterceptor` 코루틴 컨텍스트는 코루틴이 중단되었을 때 `interceptContinuation` 메서드로 컨티뉴에이션 객체를 수정하고 포장한다. `releaseInterceptedContinuation`메서드는 컨티뉴에이션이 종료되었을 때 호출된다.

ContinuationInterceptor

 

디스패처는 특정 스레드 풀에서 실행되는 `DispatchedContinuation`으로 컨티뉴에이션 객체를 래핑 하기 위해 `interceptContinuation`을 사용한다. `DispatchedContinuation`은 디스패처가 작동하는 핵심 요소이다.

 

작업의 종류에 따른 각 디스패처의 성능 비교

다음은 디스패처 및 스레드에 따라 평균 실행 시간을 나타낸다.

  중단 블로킹 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

 

이로써

1. 중단에서는 스레드 수에 상관이 없다.

2. 블로킹을 할 경우 스레드 수가 많을수록 유리하다.

3. CPU 집약적인 연산에서는 `Dispatchers.Default`가 유리하다.

4. 메모리 집약적인 연산에서, 더 많은 스레드를 사용하는 것이 낫다.

 

요약

- `Dispatchers.Default` 는 CPU 집약적인 연산에 사용

- `Dispatchers.Main`은 Android, Swing JavaFX에서 메인 스레드에 접근할 때 사용

- `Dispatchers.Main.immediate`는 `Dispatchers.Main`이 사용하는 스레드에서 실행되지만 꼭 필요할 때만 재배정됨

- `Dispatchers.IO` 는 블로킹 연산을 할 때 사용

- 병렬 처리를 제한한 `Dispatchers.IO`나 특정 스레드 풀을 사용하는 커스텀 디스패처는 블로킹 호출이 아주 많을 때 사용

- 병렬처리를 1로 제한하면 경쟁 상태를 임시로 해결할 수 있다.

- `Dispatchers.Unconfined`는 코루틴이 실행될 스레드에 대해 신경 쓸 필요가 없을 때 사용

 

 

참고자료

마르친 모스카와(2023), 코틀린 코루틴 Kotlin Coroutines : Deep Dive - 안드로이드 및 백엔드 개발자를 위한 비동기 프로그래밍, 인사이트

 

https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html

 

Coroutine context and dispatchers | Kotlin

 

kotlinlang.org