본문 바로가기
Develop/Reactor+Coroutine

[Coroutine] 코루틴 디스패처

by 연로그 2024. 7. 8.
반응형

코루틴 스터디를 하며 정리 중인 시리즈 글

  1. 왜 코루틴을 써야할까?
  2. [Coroutine] 코루틴 빌더, 코루틴 컨텍스트
  3. [Coroutine] 구조화된 동시성
  4. [Coroutine] 코루틴 스코프 함수
  5. [Coroutine] 디스패처
  6. [Coroutine] 코루틴과 공유상태

 


💥 디스패처

 

코루틴 컨텍스트에 대해 학습할 때 디스패처에 대해 간단히 학습하고 넘어갔었다. 이 글에서는 디스패처를 좀더 자세히 알아볼 예정이다. 영단어 디스패처는 사람이나 차량 등을 필요한 곳에 보내는 것을 담당하는 사람이라는 뜻이다. 코루틴에서의 디스패처는 코루틴을 스레드로 보내는 역할(어떤 환경에서 실행될지 결정하는 역할)을 한다.

dispatcher 뜻

 

 

📌 CoroutineDispatcher의 동작

CoroutineDispatcher는 아래와 같은 그림처럼 동작한다. CoroutineDispatcher 객체는 스레드풀을 사용할 수 있고, 이 스레드풀은 2개의 스레드로 구성되어 있다. CoroutineDispatcher은 작업 대기열을 갖고 있는데, 이 작업 대기열에는 실행되어야 하는 작업들이 저장될 수 있다.

 

 

  1. CoroutineDispatcher 객체에 코루틴의 실행이 요청된다.
  2. CoroutineDispatcher 객체는 실행 요청받은 코루틴을 작업 대기열에 적재한다.
  3. CoroutineDispatcher 객체는 자신이 사용 가능한 스레드가 있는지 확인한다.
  4. 작업 대기열에 적재된 코루틴을 사용 가능한 스레드로 보내 실행시킨다.

 


💥 제한된 디스패처; Confined Dispatcher

 

디스패처는 크게 제한된 디스패처와 무제한 디스패처로 나뉠 수 있다. 제한된 디스패처는 사용 가능한 스레드나 스레드풀이 제한되어있다. 제한된 디스패처의 종류는 대표적으로 아래와 같다.

디스패처 특징
Dispatchers.Default - 기본적으로 설정되는 디스패처
- CPU 집약적인 연산 수행에 적절 (CPU 집약적인 연산: 대용량 데이터 정렬, 알고리즘 실행 등)
Dispatchers.IO - I/O 연산 수행에 적절 (I/O 연산: 파일 시스템 접근, 외부 API 호출 같은 네트워크 통신 등)
Dispatchers.Main - UI가 있는 애플리케이션에서 메인 스레드 사용을 위해 사용
- kotlinx-coroutines-android 등 별도 라이브러리 추가해야 사용 가능

 

제한된 디스패처를 직접 생성할 수도 있다. newFixedThreadPoolContext같이 코루틴 라이브러리에서 제공하는 함수도 있고, java.util.concurrent.Executor를 asCoroutineDispatcher 확장함수를 통해 CoroutineDispatcher로 변환할 수 있다. 

val dispatcher = Executors.newScheduledThreadPool(10).asCoroutineDispatcher()

 

다만 asCoroutineDispatcher 함수를 이용한 경우, close 함수를 명시적으로 호출하여 메모리 누수를 방지해야 한다.

suspend fun main() = coroutineScope {
    val dispatcher = Executors.newScheduledThreadPool(10).asCoroutineDispatcher()
    repeat(20) {
        launch(dispatcher) {
            Thread.sleep(1)
            println(Thread.currentThread().name)
        }
    }
//    dispatcher.close()  // -> 여기서 close를 하지 않으면 프로그램이 종료되지 않는다.
}

 

📌 limitedParallelism 함수

limitedParallelism 함수를 이용하면 디스패처의 스레드 사용을 제한할 수 있다. 스레드 사용을 왜 제한해야 할까? 예를 들어 Dispatchers.default를 사용해 무겁고 오래 걸리는 연산을 처리한다고 가정해보자. 이 무거운 연산을 위해 Dispatchers.Default의 모든 스레드가 사용된다면? Dispatchers.Default를 사용해야하는 다른 연산들이 실행하지 못하는 상황에 올 수 있다. 이를 방지하기 위해 사용할 수도 있다고 한다.

 

Dispatchers.Default와 Dispatchers.IO는 같은 스레드풀을 공유하고 있다. 코루틴 라이브러리는 애플리케이션 레벨의 공유 스레드풀을 제공하고 있다. 이 공유 스레드풀은 스레드를 무제한 생성할 수 있으며, 스레드 생성 및 사용을 위한 API를 제공한다. Dispatchers.Default, Dispatchers.IO는 이 코루틴 라이브러리가 제공하는 API를 사용해 구현됐기 때문에 같은 스레드풀을 사용하는 것이다. 그림을 그려보자면 아래와 같다.

 

 

만약에 Dispatchers.Default를 limitedParallelism 함수를 통해 제한한다면 아래와 같은 그림이 된다. Dispatchers.Default 내의 스레드를 일부만 사용한다.

 

 

Dispatchers.IO의 limitedParallelism 함수는 약간 다르다. Dispatchers.IO 가 가진 스레드가 아닌, 공유 스레드풀에서 스레드를 가져와 새로운 스레드 풀을 만들어낸다. Dispatchers.IO의 limitedParallelism을 사용하는 이유는 특정 작업이 다른 작업에 영향받지 않아야 할 때, 별도 스레드 풀에서 실행되는 것이 필요할 때 사용한다. 다만, 새로운 스레드를 만들어내는 작업은 비싼 작업이므로 무작위로 남용하지 않도록 주의해야 한다.

 

 


💥 무제한 디스패처; Unconfined Dispatcher

 

무제한이라는 말만 보면 스레드가 무제한으로 제공되는 디스패처인가? 라는 착각에 빠지기 쉽다. 무제한 디스패처는 코루틴이 자신을 생성한 스레드에서 즉시 실행하도록 만드는 디스패처다. 이때 호출된 스레드가 무엇이든지 상관없다. 실행 스레드의 종류가 제한되지 않는다, 한계가 없다는 맥락에서 무제한 디스패처라는 이름이 지어진 것 같다.

 

아래 예제 코드와 결과를 살펴보면 launch(Dispatchers.Unconfied)를 실행한 스레드, 즉 main 스레드를 통해 실행되는 것을 볼 수 있다. 무제한 디스패처를 사용하는 경우, 스레드 스위칭 없이 즉시 실행된다.

suspend fun main() = coroutineScope {
    println(Thread.currentThread().name)
    launch(Dispatchers.Unconfined) { // 무제한 디스패처 사용
        println(Thread.currentThread().name)
    }
    launch {
        println(Thread.currentThread().name)
    }
}

실행 화면

 

단, 무제한 디스패처를 사용하는 코루틴에서 중단이 일어나면 재개 시에는 자신을 재개시키는 스레드에서 실행된다. 아래 예제에서는 DefaultExecutor를 통해 실행되는 것으로 보이는데, 이는 delay 함수를 실행하는 스레드이다.

suspend fun main() = coroutineScope {
    println(Thread.currentThread().name)
    launch(Dispatchers.Unconfined) { // 무제한 디스패처 사용
        println(Thread.currentThread().name)
        delay(10)
        println(Thread.currentThread().name)
    }
}

실행 화면

 

📌 CoroutineStart.UNDISPATCHED vs Unconfined Dispatcher

코루틴 빌더에는 CoroutineStart라는 인자를 받을 수 있다. 이 중에서 UNDISPATCHED라는 옵션이 있는데 무제한 디스패처와 유사한 점이 있다. 둘 다 호출자의 스레드에서 직접 실행일시 중단 후 재개한다는 공통점을 갖고 있다. 하지만 코루틴이 일시중단되고 다시 재개할 때 동작 방식이 다르다.

- CoroutineStart.UNDISPATCHED: CoroutineDispatcer 객체를 거쳐 실행
- Unconfined Dispatcher: 재개시킨 스레드에서 직접 실행

참고

반응형