Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Archives
Today
Total
관리 메뉴

Life Engineering

코루틴 디스패처(Coroutine Dispatcher) 본문

공부/Kotlin

코루틴 디스패처(Coroutine Dispatcher)

흑개 2024. 6. 20. 22:36

코루틴 디스패처 (Coroutine Dispatcher)

코루틴 디스패처란?

  • 코루틴을 스레드로 보내 실행시키는 역할, 코루틴을 스레드로 보내는 데 사용할 수 있는 스레드나 스레드 풀을 가짐
  • 실행 요청한 스레드에서 코루틴이 실행될 수 있게 함

CoroutineDistpatcher 는 코루틴의 실행을 관리하는 주체로

실행 요청된 코루틴을 작업 대기열에 적재하고

스레드가 새로운 작업을 시작할 수 있는 상태라면 스레드에 코루틴을 보내 실행함

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

  • 제한된 디스패처: 사용할 수 있는 스레드, 스레드풀이 제한
  • 무제한 디스패처: 사용할 수 있는 스레드, 스레드풀이 제한되지 않음 → 실행 요청된 코루틴이 이전 코드가 실행된 스레드에서 계속 실행 됨

제한된 디스패처 생성

  • Single-Thread Dispatcher: 사용할 수 있는 스레드가 1개인 디스패처, name 은 디스패처에서 관리하는 스레드의 이름
fun main() = runBlocking<Unit> {
    val dispatcher = newSingleThreadContext(name="SingleThread")
    //val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispacher()
    launch(context = dispatcher){
        println("[${Thread.currentThread().name}] 실행")
    }
}

//[SingleThread] 실행
  • Multi-Thread Dispatcher: 2개 이상의 스레드 사용할 수 있는 디스패처, newFixedThreadPoolContext 사용
import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
  val multiThreadDispatcher = newFixedThreadPoolContext(
    nThreads = 2,
    name = "MultiThread"
  )
  launch(context = multiThreadDispatcher) {
    println("[${Thread.currentThread().name}] 실행")
  }
  launch(context = multiThreadDispatcher) {
    println("[${Thread.currentThread().name}] 실행")
  }
}

//[MultiThread-1 @coroutine#2] 실행
//[MultiThread-2 @coroutine#3] 실행

부모 - 자식 CoroutineDispatcher

구조화를 제공해 코루틴 내부에서 새로운 코루틴 실행 가능

바깥쪽 코루틴을 부모 코루틴, 내부에서 생성되는 새로운 코루틴을 자식 코루틴이라고 함

부모 코루틴의 실행 환경을 자식 코루틴의 실행 환경으로 전달 가능

→ 자식 코루틴의 CoroutineDispatcher 객체가 생성되지 않았으면 부모 코루틴의 CoroutineDispatcher 객체를 사용하게 됨

fun main() = runBlocking<Unit> {
  val multiThreadDispatcher = newFixedThreadPoolContext(
    nThreads = 2,
    name = "MultiThread"
  )
  launch(multiThreadDispatcher) { // 부모 Coroutine
    println("[${Thread.currentThread().name}] 부모 코루틴 실행")
    launch { // 자식 코루틴 실행
      println("[${Thread.currentThread().name}] 자식 코루틴 실행")
    }
    launch { // 자식 코루틴 실행
      println("[${Thread.currentThread().name}] 자식 코루틴 실행")
    }
  }
}

//[MultiThread-1 @coroutine#2] 부모 코루틴 실행
//[MultiThread-2 @coroutine#3] 자식 코루틴 실행
//[MultiThread-1 @coroutine#4] 자식 코루틴 실행

미리 정의된 CoroutineDispatcher

미리 정의된 디스패처가 있는 이유?:

newFixedThreadPoolContext 사용해서 CoroutineDispatcher 객체를 만들 경우,

  • 특정 CoroutineDispatcher 객체에서만 사용되는 스레드풀이 생성되고 스레드풀에 속한 스레드의 수가 너무 적거나 많이 생성되면 비효율적으로 동작 할 수 있다
  • 특정 CoroutineDispatcher가 이미 메모리 상에 존재하는 데도 해당 객체의 존재를 몰라 다시 CoroutineDispatcher 객체를 만들어 리소스를 낭비하게 될 수 있다

⇒ 따라서 미리 정의된 CoroutineDispatcher 가 존재한다

Dispatchers.IO

네트워크 요청이나 파일 입출력 등의 I/O 작업을 위한 CoroutineDispatcher

I/O 연산으로 스레드를 블로킹 할 때 사용하기 위해 설계됨

스레드의 수는 JVM에서 사용 가능한 프로세서의 수(코어 수)와 64 중 큰 값으로 설정

아래 코드는 Dispatchers.IO가 같은 시간에 50개가 넘는 스레드를 사용할 수 있도록 만들어져 1초 내외가 걸림

Dispatchers.IO를 사용해야 하는 가장 흔한 경우는 라이브러리에서 블로킹 함수를 호출할 경우

이런 경우 withContext(Dispatchers.IO) 로 래핑해 suspending function 으로 만들어야 함

class DiscUserRepository(
    private val discReader: DiscReader
) : UserRepository {
    override suspend fun getUser(): UserData =
        withContext(Dispatchers.IO) {
            UserData(discReader.read("userName"))
        }
}

 

스레드를 블로킹하는 API 를 사용할 경우, Dispatchers.IO 가 주로 필요함

 

suspending function을 제공하는 network call or database library 가 있으면, Dispatchers.IO를 사용할 필요 없다. suspending function 이 있으면 어떤 dispatchers 나 사용 가능하다.
즉 많은 프로젝트에서 Dispatchers.IO는 많이 사용할 필요가 없을 지도?

 

BUT 스레드를 많이 블로킹 하는 서비스가 Dispatchers.IO 의 스레드를 모두 점유한다면?

limitedParallelism 사용하여 독립적인 스레드 풀을 가진 새로운 디스패처를 만듦

위 코드에서 두 디스패처의 한도는 서로 무관함

64개의 스레드를 가진 Dispatchers.IO는 각 스레드를 1초씩 블로킹하는 코루틴 100개가 실행될 때 2초 소요, BUT 100개의 스레드를 가진 limitedParallelism 사용하는 Dispatchers.IO는 같은 조건의 코루틴이 100개 실행될 때 1초 소요

 

 

→ limitedParalleism 을 잘 활용하는 방법은 스레드를 블로킹하는 경우가 잦은 클래스에서 자신만의 한도를 가진 커스텀 디스패처를 정의하는 것

이때 정의되는 커스텀 디스패처는 사용되는 스레드 한도가 Dispatchers.IO를 비롯한 다른 디스패처와 무관하기 때문에 한 서비스가 다른 서비스를 블로킹하는 경우는 없음

스레드 한도를 어떻게 정하나?

  • Performance 우선한다면 대부분의 시간에 사용되는 스레드 수에 대한 제한 설정
  • 리소스 사용량 우선한다면 피크 타임에 사용될 스레드 수에 대한 제한 설정
class NewsletterService {
    private val dispatcher = Dispatchers.IO.limitedParallelism(5)
    private val sendGrid = SendGrid(API_KEY)

    suspend fun sendNewsletter(
        newsletter: Newsletter,
        emails: List<Email>
    ) = withContext(dispatcher) {
        emails.forEach { email ->
            launch {
                sendGrid.api(createNewsletter(email, newsletter))
            }
        }
    }

    // ...
}

class AuthorizationService {
    private val dispatcher = Dispatchers.IO.limitedParallelism(50)

    suspend fun sendAuthEmail(
        user: User
    ) = withContext(dispatcher) {
        sendGrid.api(createConfirmationEmail(user))
    }

    // ...
}

위의 코드에서, NewsLetter 전송하는 경우 시간이 많이 걸려도 상관 없음, 5 개로 설정

RegisterConfirmation Email 전송하는 경우 최대한 빨리 보내야 함, 또한 집중적으로 사용될 서비스가 아니기 때문에 50개로 설정

 

 

아래는 공유 스레드풀을 나타낸 그림

Dispatchers.IO 와 Dispatchers.Default는 모두 공유 스레드풀을 사용한다

Dispatchers.DefaultlimitedParalleism 은 디스패처에 스레드 제한을 추가,
Dispatchers.IOlimitedParalelleism 은 Dispatchers.IO 와 독립적인 디스패처를 만듦
모든 디스패처는 스레드가 무제한인 스레드 풀을 함께 공유함

Dispatchers.Default

대용량 계산 등 CPU-Bound 한 작업을 위한 CoroutineDispatcher

코드가 실행되는 컴퓨터의 CPU 개수와 동일한 수(최소 2개 이상)의 스레드 풀을 가짐

비용이 많이 드는 작업이 Dispatchers.Default의 스레드를 모두 점유해서 같은 스레드를 쓰는 다른 작업이 실행되지 못할 때,

limitedParallelism 를 사용하면 같은 스레드 풀을 사용하지만 특정 수 이상의 스레드를 사용하지 못하도록 할 수 있다

Dispatchers.Main

안드로이드에서 메인 스레드는 UI와 상호작용하는 데 사용하는 유일한 스레드

메인 스레드에서 코루틴을 실행할 때 사용되는 디스패처

kotlinx-coroutines-android 아티팩트 사용하면 해당 디스패처 사용 가능

싱글 스레드로 제한된 디스패처

동일 시간에 다수의 스레드가 공유 상태를 변경한다면 ?

→ 싱글스레드를 가진 디스패처를 사용하여 공유 상태로 변경으로 인한 문제를 방지할 수 있다

val dispatcher = Executors.newSingleThreadExecutor()
    .asCoroutineDispatcher()

위와 같이 Executors를 사용하여 싱글 스레드를 만들 수 있지만,

스레드 하나를 액티브한 상태로 유지하고 있어야 하며, 이 디스패처가 사용되지 않을 때는 스레드를 반드시 닫아야 함(close 함수 호출)

limitedParalleism(1) 사용해 병렬 처리를 1로 제한한 Dispatchers.IO나 Dispatchers.Default 사용함

BUT 스레드가 블로킹 되면 작업이 순차적으로 처리되는 것이 단점

각 Dispatcher의 성능 비교

(성능 비교 코드: https://kt.academy/dispatchers-benchmarks)

위 표는

Suspending: 1초 동안 중단하는 작업

Blocking: 1초 동안 블로킹되는 작업

Memory: Memory-intensive 작업(메모리 접근, 할당, 해제 .. 등)

CPU: CPU-intensvie 작업

위와 같은 작업을 수행하는 100개의 독립적인 코루틴을 실행한 결과임

  • 중단할 경우(suspending function)에는 사용하고 있는 스레드가 얼마나 많은지 중요하지 않음
  • 블로킹할 경우에는 스레드 수가 많을 수록 모든 코루틴이 종료되는 시간이 빨라짐
  • CPU 집약적인 연산에는 Dispatchers.Default 가 가장 좋음
  • 메모리 집약적인 연산을 수행한다면 더 많은 스레드를 사용 하는 것이 낫다

'공부 > Kotlin' 카테고리의 다른 글

코루틴 컨텍스트 (Coroutine Context)  (0) 2024.06.21
코루틴 빌더 기초  (0) 2024.06.14
Kotlin 코루틴의 기본  (1) 2024.06.09