Kotlin/코틀린 코루틴의 정석

[Kotlin] CoroutineDispatcher 정리

꾸준함. 2024. 11. 5. 00:28

Dispatcher란?

  • dispatch와 -er의 합성어
  • dispatch의 `보내다`라는 뜻에 -er이 붙어 `무언가를 보내는 주체`라는 뜻

 

CoroutineDispatcher

  • `코루틴을 보내는 주체`
  • 코루틴은 일시 중단이 가능한 `작업`이기 때문에 쓰레드가 있어야 실행될 수 있음
  • CoroutineDispatcher는 코루틴을 쓰레드로 보내 실행시키는 역할
    • 코루틴을 쓰레드로 보내는 데 사용할 수 있는 쓰레드나 쓰레드 풀 가짐
    • 코루틴을 실행 요청한 쓰레드에서 코루틴이 실행되도록 만들 수 있음

 

CoroutineDispatcher 동작 과정

  • CoroutineDispatcher 객체에 코루틴의 실행이 요청됨
  • CoroutineDispatcher 객체는 실행 요청받은 코루틴을 작업 대기열에 적재시킴
  • CoroutineDispatcher 객체는 자신이 사용 가능한 쓰레드가 있는지 확인
  • 작업 대기열에 적재된 코루틴을 사용 가능한 쓰레드로 보내 실행

 

* 사용 가능한 쓰레드가 없을 경우 작업 대기열에서 대기

 

제한된 디스패처와 무제한 디스패처

  • CoroutineDispatcher에는 두 가지 종류가 존재
    • 제한된 디스패처 (Confined Dispatcher)
    • 무제한 디스패처 (Unconfined Dispatcher)

 

1. 제한된 디스패처

  • 사용할 수 있는 쓰레드나 쓰레드 풀이 제한된 디스패처
  • 앞서 `CoroutineDispatcher 동작 과정`에서 설명한 대로 동작함
  • 일반적으로 CoroutineDispatcher 객체별로 어떤 작업을 처리할지 미리 역할을 부여하고 역할에 맞춰 실행을 요청하는 것이 효율적이기 때문에 대부분의 CoroutineDispatcher 객체는 제한된 디스패처

 

2. 무제한 디스패처

  • 사용할 수 있는 쓰레드나 쓰레드 풀이 제한되지 않은 디스패처
  • 실행할 수 있는 쓰레드가 제한되지 않았다고 해서 실행 요청된 코루틴이 아무 쓰레드에서나 실행되는 것은 아님
  • 무제한 디스패처는 실행 요청된 코루틴이 이전 코드가 실행되던 쓰레드에서 계속해서 실행되도록 처리
    • 이 때문에 실행되는 쓰레드가 매번 달라질 수 있고 특정 쓰레드로 제한되어 있지 않음

 

제한된 디스패처 생성하는 방법

  • 코루틴 라이브러리는 사용자가 직접 제한된 디스패처를 만들 수 있도록 몇 가지 함수를 제공

 

1. 단일 쓰레드 디스패처 생성

  • 사용할 수 있는 쓰레드가 하나인 CoroutineDispatcher 객체
  • 코루틴 라이브러리에서 제공하는 `newSingleThreadContext` 함수를 사용해 생성 가능
    • `newFixedThreadPoolContext` 함수에 쓰레드 개수 인자로 1을 전달하면 동일하게 생성 가능

 

 

 

2. 멀티 쓰레드 디스패처 생성

  • 두 개 이상의 쓰레드를 사용할 수 있는 CoroutineDispatcher 객체
  • 코루틴 라이브러리의 newFixedThreadPoolContext 함수를 사용해 생성 가능
  • 만들어지는 쓰레드들은 인자로 받은 name 값 뒤에 `-1`부터 시작해 숫자가 하나씩 증가하는 형식으로 명명

 

 

CoroutineDispatcher 사용해 코루틴 실행하는 방법

 

1. launch의 파라미터로 CoroutineDispatcher 사용

  • launch 함수를 호출해 만든 코루틴을 특정 CoroutineDispatcher 객체에 실행 요청하기 위해서는 launch 함수의 context 인자로 CoroutineDispatcher 객체를 넘기면 됨

 

 

2. 부모 코루틴의 CoroutineDispatcher을 사용해 자식 코루틴 실행

  • 코루틴은 구조화를 제공해 코루틴 내부에서 새로운 코루틴 실행 가능
    • 바깥쪽의 코루틴을 부모 코루틴(Parent Coroutine)이라고 지칭
    • 내부에서 생성되는 새로운 코루틴을 자식 코루틴(Child Coroutine)이라고 지칭

 

  • 자식 코루틴에 CoroutineDispatcher 객체가 설정되지 않았을 경우 부모 코루틴의 CoroutineDispatcher 객체를 사용

 

 

부연 설명

  • 자식 코루틴에는 별도 CoroutineDispatcher 객체가 설정돼 있지 않으므로 부모 코루틴에 설정된 CoroutineDispatcher 객체를 사용
    • 부모 코루틴과 자식 코루틴이 모두 같은 CoroutineDispatcher 객체를 사용하므로 MultiThread-1과 MultiThread-2를 공용으로 사용하는 것을 볼 수 있음

 

미리 정의된 CoroutineDispatcher

  • newFixedThreadPoolContext 함수를 사용해 직접 CoroutineDispatcher 객체를 생성할 경우 특정 CoroutineDispatcher 객체에서만 사용되는 쓰레드들이 생성되며, 쓰레드 풀에 속한 쓰레드의 수가 너무 적거나 많이 생성되어 비효율적으로 동작할 확률 존재
  • 코루틴 라이브러리에서 제공되는 CoroutineDispatcher 객체들은 멀티 쓰레드 프로그래밍이 필요한 일반적인 상황에 맞춰 만들어졌기 때문에 사용자들이 매번 새로운 CoroutineDispatcher 객체를 만들 필요 없이 제공되는 CoroutineDispatcher 객체를 사용해 코루틴을 실행할 수 있도록 미리 정의된 (제한된) CoroutineDispatcher들의 목록을 다음과 같이 제공함
    • Dispatchers.IO
    • Dispatchers.Default
    • Dispatcher.Main

 

1. Dispatchers.IO

  • 멀티 쓰레드 프로그래밍이 가장 많이 사용되는 작업은 I/O 작업
    • ex) 애플리케이션 내 네트워크 통신을 위해 HTTP 요청을 하거나 DB 작업 같은 입출력 작업 여러 개를 동시에 수행하기 위해서는 많은 쓰레드 필요

 

  • 코루틴 라이브러리에서는 입출력 작업을 위해 미리 정의된 Dispatchers.IO를 제공
    • 코루틴 라이브러리 1.7.2 버전을 기준으로 Dispatchers.IO가 최대로 사용할 수 있는 쓰레드의 수는 {JVM에서 사용이 가능한 프로세서의 수}와 64 중 더 큰 값으로 설정 가능
    • Dispatchers.IO는 싱글톤 인스턴스이므로 launch 함수의 인자로 곧바로 넘겨 사용 가능


 

부연 설명

  • DefaultDispatcher-worker가 쓰레드명의 접두사로 붙은 쓰레드는 코루틴 라이브러리에서 제공하는 공유 쓰레드 풀에 속한 쓰레드
  • Dispatchers.IO와 Dispatchers.Default는 공유 쓰레드 풀의 쓰레드를 사용할 수 있도록 구현되어 있음

 

2. Dispatchers.Default

  • 대용량 데이터를 처리해야 하는 작업처럼 CPU 연산이 필요한 작업도 존재하며 이러한 작업을 CPU 바운드 작업이라고 지칭
    • CPU 바운드 작업은 작업을 하는 동안 쓰레드를 지속적으로 사용하므로 쓰레드 기반 작업을 사용해 실행했을 때와 코루틴을 사용해 실행했을 때와 처리 속도면에 큰 차이 없음
    • 반면, IO 작업의 경우 작업을 실행한 후 결과를 반환받을 때까지 쓰레드 점유를 하지 않기 때문에 코루틴 사용할 경우 처리 속도면에서 유의미하게 차이가 나는 것을 확인 가능

 

  • Dispatchers.Default는 CPU 바운드 작업이 필요할 때 사용하는 CoroutineDispatcher
  • Dispatchers.Default도 그 자체로 싱글톤 인스턴스이므로 Dispatchers.IO와 마찬가지로 launch 함수의 인자로 곧바로 넘겨 사용 가능

 

2.1 limitedParallelism을 통해 Dispatchers.Default 쓰레드 사용 제한

  • Dispatchers.Default를 사용해 무겁고 오래 걸리는 연산을 처리할 경우 특정 연산을 위해 Dispatchers.Default의 모든 쓰레드가 사용될 수도 있으며 이 경우 해당 연산이 모든 쓰레드를 사용하는 동안 Dispatchers.Default를 사용하는 다른 연산이 실행되지 못함
    • 이를 방지하기 위해 코루틴 라이브러리는 Dispatchers.Default의 일부 쓰레드만 사용해 특정 연산을 실행할 수 있도록 limitedParallelism 함수 지원


 

부연 설명

  • Dispatchers.Default의 여러 쓰레드 중 두 개의 쓰레드만 사용해 10개의 코루틴을 실행시키도록 limitedParallelism 적용

 

3. Dispatchers.Main

  • 일반적으로 UI가 있는 애플리케이션에서 메인 쓰레드의 사용을 위해 사용되는 특별한 CoroutineDispatcher 객체
    • Dispatchers.IO나 Dispatchers.Default와는 성격이 다름
    • 코루틴 라이브러리에 대한 의존성만 추가한다고 해서 Dispatchers.Main을 사용할 수 없고 별도 라이브러리인 `kotlinx-coroutines-android` 등을 추가해야 사용 가능

 

비고

 

1. 공유 쓰레드 풀

  • 앞서 언급했다시피 Dispatchers.IO와 Dispatchers.Default가 같은 쓰레드 풀인 공유 쓰레드 풀을 사용하므로 코루틴을 실행시킨 쓰레드의 접두사가 DefaultDispatcher-worker인 것을 확인 가능
  • 코루틴 라이브러리는 쓰레드의 생성과 관리를 효율적으로 할 수 있도록 애플리케이션 레벨의 공유 쓰레드 풀 제공
    • 쓰레드를 무제한으로 생성 가능
    • 코루틴 라이브러리는 공유 쓰레드 풀에 쓰레드를 생성하고 사용할 수 있도록 API 제공

 

  • newFixedThreadPoolContext 함수로 만들어진 디스패처가 자신만 사용할 수 있는 전용 쓰레드 풀을 생성하는 것과 다르게 Dispatchers.IO와 Dispatchers.Default는 공유 쓰레드 풀의 쓰레드를 사용

 

 

2. Dispatchers.IO의 limitedParallelism

  • Dispatchers.Default에서 limitedParllelism 함수를 사용하면 위 사진처럼 Dispatchers.Default가 사용할 수 있는 쓰레드 중 일부만을 사용할 수 있음
  • Dispatchers.IO의 limitedParallelism 함수는 공유 쓰레드 풀의 쓰레드로 구성된 새로운 쓰레드 풀을 생성하며, 만들어낼 수 있는 쓰레드에 제한이 있는 Dispatchers.IO나 Dispatchers.Default와 달리 쓰레드의 수를 제한 없이 만들 수 있음
    • 특정한 작업이 다른 작업에 영향을 받지 않도록 별도 쓰레드 풀에서 실행되는 것이 필요할 때 사용
    • 별도 쓰레드 풀을 생성하는 것은 비싼 작업이므로 남용 X

 

참고

코틀린 코루틴의 정석 (조세영 저)

반응형