관리 메뉴

피터의 개발이야기

[kotlin] 코틀린 코루틴의 정석- CoroutineDispatcher 본문

Programming/Kotlin

[kotlin] 코틀린 코루틴의 정석- CoroutineDispatcher

기록하는 백앤드개발자 2024. 5. 29. 10:10
반응형

ㅁ 들어가며

코틀린 코루틴의 정석 책을 보고 정리한 글입니다.

 

2장 정리

ㅇ 현재 실행 중인 스레드의 이름 출력: Thread.currentThread().name
ㅇ 스레드의 이름 출력: JVM의 VM options에 -Dkotlinx.coroutines.debug

ㅇ CoroutineName으로 코루틴의 이름을 지정

 

ㅁ 실행 중인 코루틴의 이름 출력 VM option

-Dkotlinx.coroutines.debug

[main @coroutine#1] Start]

종료 코드 0(으)로 완료된 프로세스

ㅇ 스레드 출력 시 코루틴의 이름이 표출된다.

 

ㅁ launch로  개별 스레트 할당하기

fun main() = runBlocking<Unit> {
    println("[${Thread.currentThread().name}] 실행1")
    launch {
        println("[${Thread.currentThread().name}] 실행2")
    }
    launch {
        println("[${Thread.currentThread().name}] 실행3")
    }
}

/*
[main @coroutine#1] 실행1
[main @coroutine#2] 실행2
[main @coroutine#3] 실행3
*/

ㅇ main과 2개의 launch가 생성한 스래드에 의해 병렬처리가 되었다.

 

ㅁ Coroutine 이름 지정

...
fun main() = runBlocking<Unit>(context = CoroutineName("Peterica")) {
...

ㅇ CoroutineName을 Peterica로 지정하였다.

 

3장 CoroutineDispatcher

스레프풀 스케줄러

  Dispatcher의 뜻은 무언가를 보내는 주체를 말한다. CoroutineDispatcher는 스레드풀을 생성하여 코루틴을 스레드로 보내 실행시키는 역할을 한다. 자신에게 실행 요청된 코루틴들을 큐에 적재하고, 유휴스레드에 스케줄링하는 역할을 한다.

CoroutineDispatcher의 종류

  CoroutineDispatcher는 스레드풀을 제한하는 제한된 디스패처와 무제한 디스패처가 있다. 

 

ㅁ 제한된 디스패처 만들기

// Single
val dispatcher: CoroutineDispatcher = newSingleThreadContext(name = "SingleThread")

// multi
val multiThreadDispatcher: CoroutineDispatcher = newFixedThreadPoolContext(
  nThreads = 2,
  name = "MultiThread"
)

 

 

ㅁ launch로 dispatcher 사용하기

val dispatcher = newSingleThreadContext(name = "SingleThread")
launch(dispatcher) {
  println("[${Thread.currentThread().name}] SingleThread라고 dispatcher의 스레드 이름이 보인다.")
}

// 출력
[SingleThread] SingleThread라고 dispatcher의 스레드 이름이 보인다.

종료 코드 0(으)로 완료된 프로세스

ㅇ 스레드풀이 생성된 dispatcher를 받아 launch가 println을 실행시킨다.

 

ㅁ launch로 multi dispatcher 사용하기

  val multiThreadDispatcher = newFixedThreadPoolContext(
    nThreads = 2,
    name = "MultiThread"
  )
  launch(context = multiThreadDispatcher) {
    delay(1000)  // 지연발생
    println("[${Thread.currentThread().name}] 실행1")
  }
  launch(context = multiThreadDispatcher) {
    delay(1100) // 지연발생
    println("[${Thread.currentThread().name}] 실행2")
  }
  launch(context = multiThreadDispatcher) {
    println("[${Thread.currentThread().name}] 실행3")
  }
/* 출력
[MultiThread-1] 실행3
[MultiThread-2] 실행1
[MultiThread-1] 실행2
*/

 

ㅇ dispatcher가 개별 스레드 잡에 지연이 발생하면 스케줄관리를 하여 실행3이 우선 실행하도록 한다. 

 

ㅁ 부모와 자식루틴 

  val dataFormat = SimpleDateFormat("yyyy-MM-dd hh:mm:ss") // 시간 로그용
  val multiThreadDispatcher = newFixedThreadPoolContext(
    nThreads = 2,
    name = "MultiThread"
  )
  launch(multiThreadDispatcher) { // 부모 Coroutine
    println("[${Thread.currentThread().name}] 부모 코루틴 실행")
    val childThread = newFixedThreadPoolContext(
      nThreads = 2,
      name = "childThread"
    )
    // 자식 코루틴 실행 
    for (i in 1..20) {
      launch(childThread) { 
        delay(i.toLong()*100)
        println("[${dataFormat.format(System.currentTimeMillis())}][${Thread.currentThread().name}] 자식 코루틴 실행${i}")
      }
    }
  }
/* 출력
[MultiThread-1 @coroutine#2] 부모 코루틴 실행
[2024-05-22 04:58:18][childThread-2 @coroutine#3] 자식 코루틴 실행1
[2024-05-22 04:58:18][childThread-1 @coroutine#4] 자식 코루틴 실행2
[2024-05-22 04:58:18][childThread-2 @coroutine#5] 자식 코루틴 실행3
[2024-05-22 04:58:18][childThread-1 @coroutine#6] 자식 코루틴 실행4
[2024-05-22 04:58:18][childThread-2 @coroutine#7] 자식 코루틴 실행5
[2024-05-22 04:58:18][childThread-1 @coroutine#8] 자식 코루틴 실행6
[2024-05-22 04:58:18][childThread-2 @coroutine#9] 자식 코루틴 실행7
[2024-05-22 04:58:19][childThread-1 @coroutine#10] 자식 코루틴 실행8
[2024-05-22 04:58:19][childThread-2 @coroutine#11] 자식 코루틴 실행9
[2024-05-22 04:58:19][childThread-1 @coroutine#12] 자식 코루틴 실행10
[2024-05-22 04:58:19][childThread-2 @coroutine#13] 자식 코루틴 실행11
[2024-05-22 04:58:19][childThread-1 @coroutine#14] 자식 코루틴 실행12
[2024-05-22 04:58:19][childThread-2 @coroutine#15] 자식 코루틴 실행13
[2024-05-22 04:58:19][childThread-1 @coroutine#16] 자식 코루틴 실행14
[2024-05-22 04:58:19][childThread-2 @coroutine#17] 자식 코루틴 실행15
[2024-05-22 04:58:19][childThread-1 @coroutine#18] 자식 코루틴 실행16
[2024-05-22 04:58:19][childThread-2 @coroutine#19] 자식 코루틴 실행17
[2024-05-22 04:58:20][childThread-1 @coroutine#20] 자식 코루틴 실행18
[2024-05-22 04:58:20][childThread-2 @coroutine#21] 자식 코루틴 실행19
[2024-05-22 04:58:20][childThread-1 @coroutine#22] 자식 코루틴 실행20
*/

ㅇ 부모 루틴에서 자식 루틴을 20개 실행해 보았다.

ㅇ 생성된 루틴은 coroutine#{num}로 인덱싱 되어 큐에 적재되고 순차적으로 유휴 스레드에 할당되어 처리되었다.

 

ㅁ Dispatchers의 종류와 용도

ㅇ Dispatchers.IO : 네트워크 및 파일IO

 Dispatchers.Default: CPU 작업

ㅇ Dispatchers.Main : 메인 스레드를 사용

 

ㅁ Dispatchers.IO

// 지연이 있는 경우
var a = 1
  for(k in 1..20) {
    a = 1
    launch(Dispatchers.IO) {
      delay(k.toLong()*50) // 지연발생 시
      for(n in 1..100000) {
        a = a * n * n
      }
      println("[${Thread.currentThread().name}] ${k}실행")
    }
  }
/* 출력
[DefaultDispatcher-worker-2 @coroutine#2] 1실행
[DefaultDispatcher-worker-2 @coroutine#3] 2실행
[DefaultDispatcher-worker-2 @coroutine#4] 3실행
[DefaultDispatcher-worker-2 @coroutine#5] 4실행
[DefaultDispatcher-worker-2 @coroutine#6] 5실행
[DefaultDispatcher-worker-2 @coroutine#7] 6실행
[DefaultDispatcher-worker-2 @coroutine#8] 7실행
[DefaultDispatcher-worker-2 @coroutine#9] 8실행
[DefaultDispatcher-worker-2 @coroutine#10] 9실행
[DefaultDispatcher-worker-2 @coroutine#11] 10실행
[DefaultDispatcher-worker-2 @coroutine#12] 11실행
[DefaultDispatcher-worker-2 @coroutine#13] 12실행
[DefaultDispatcher-worker-2 @coroutine#14] 13실행
[DefaultDispatcher-worker-2 @coroutine#15] 14실행
[DefaultDispatcher-worker-2 @coroutine#16] 15실행
[DefaultDispatcher-worker-2 @coroutine#17] 16실행
[DefaultDispatcher-worker-2 @coroutine#18] 17실행
[DefaultDispatcher-worker-2 @coroutine#19] 18실행
[DefaultDispatcher-worker-2 @coroutine#20] 19실행
[DefaultDispatcher-worker-2 @coroutine#21] 20실행
*/

// 지연이 없는 경우
  var a = 1
  for(k in 1..20) {
    a = 1
    launch(Dispatchers.IO) {
//      delay(k.toLong()*50)
      for(n in 1..100000) {
        a = a * n * n
      }
      println("[${Thread.currentThread().name}] ${k}실행")
    }
  }
/* 출력
[DefaultDispatcher-worker-4 @coroutine#7] 6실행
[DefaultDispatcher-worker-17 @coroutine#17] 16실행
[DefaultDispatcher-worker-11 @coroutine#19] 18실행
[DefaultDispatcher-worker-12 @coroutine#11] 10실행
[DefaultDispatcher-worker-19 @coroutine#18] 17실행
[DefaultDispatcher-worker-18 @coroutine#15] 14실행
[DefaultDispatcher-worker-5 @coroutine#6] 5실행
[DefaultDispatcher-worker-25 @coroutine#20] 19실행
[DefaultDispatcher-worker-20 @coroutine#21] 20실행
[DefaultDispatcher-worker-16 @coroutine#16] 15실행
[DefaultDispatcher-worker-9 @coroutine#9] 8실행
[DefaultDispatcher-worker-2 @coroutine#4] 3실행
[DefaultDispatcher-worker-6 @coroutine#5] 4실행
[DefaultDispatcher-worker-8 @coroutine#10] 9실행
[DefaultDispatcher-worker-1 @coroutine#2] 1실행
[DefaultDispatcher-worker-7 @coroutine#8] 7실행
[DefaultDispatcher-worker-10 @coroutine#13] 12실행
[DefaultDispatcher-worker-3 @coroutine#3] 2실행
[DefaultDispatcher-worker-13 @coroutine#12] 11실행
[DefaultDispatcher-worker-14 @coroutine#14] 13실행
*/

ㅇ Dispatchers.IO는 공유 스레드를 사용하고 있는데, 얼마 정도 생성되어 있는지 알아보기 위해 루틴을 1000개 만들어 보았다.

  var a = 1
  // 1000개 루틴 생성
  for(k in 1..1000) {
    a = 1
    launch(Dispatchers.IO) {
      for(n in 1..100000) {
        a = a * n * n
      }
      println("[${Thread.currentThread().name}] ${k}실행")
    }
  }
....
[DefaultDispatcher-worker-44 @coroutine#416] 415실행
[DefaultDispatcher-worker-65 @coroutine#417] 416실행
[DefaultDispatcher-worker-47 @coroutine#429] 428실행
[DefaultDispatcher-worker-72 @coroutine#430] 429실행 << 최대
[DefaultDispatcher-worker-7 @coroutine#432] 431실행
[DefaultDispatcher-worker-31 @coroutine#433] 432실행
[DefaultDispatcher-worker-65 @coroutine#435] 434실행
[DefaultDispatcher-worker-47 @coroutine#436] 435실행
[DefaultDispatcher-worker-35 @coroutine#419] 418실행
....

 

Dispatchers.IO의 최대 스레드의 수 = JVM에서 사용이 가능한 프로세서의 수64 중 큰값

ㅇ 루틴을 1000개 정도 잡을 돌려보았을 때에 72개가 Max로 잡혔다.

 

ㅁ Dispatchers.Default

ㅇ 대용량데이터를 처리하기 위해 CPU 사용량에 맞추어진 스레드풀이다.

입출력작업과 CPU바운드작업

  ㄴ 대용량데이터 처리 시 스레드를 최대로 사용하기 때문에 코루틴과 기존 스레드 방식은 차이가 거의 없다.

  ㄴ 코루틴의 유휴스레드를 최대한 재사용하기 위해 스케줄하지만 유휴 스레드가 없다면 작업은 지연될 수 밖에 없다.

 

ㅁ limitedParallelism

launch(Dispatchers.Default.limitedParallelism(2)){
    repeat(1000) {
      launch {
        println("[${Thread.currentThread().name}] 코루틴 실행")
      }
    }
  }
출력:
[DefaultDispatcher-worker-2 @coroutine#3] 코루틴 실행
[DefaultDispatcher-worker-2 @coroutine#4] 코루틴 실행
[DefaultDispatcher-worker-2 @coroutine#5] 코루틴 실행
[DefaultDispatcher-worker-2 @coroutine#6] 코루틴 실행
~~~~~
[DefaultDispatcher-worker-2 @coroutine#999] 코루틴 실행
[DefaultDispatcher-worker-1 @coroutine#1000] 코루틴 실행
[DefaultDispatcher-worker-2 @coroutine#1001] 코루틴 실행
[DefaultDispatcher-worker-1 @coroutine#1002] 코루틴 실행

ㅇ limitedParallelism는 Dispatchers.Default의 스레드들의 과점유를 방지하기 위해  일부 스레드만 사용하도록 제한을 둔다.

  

ㅁ 공유스레드

코틀린 코루틴의 정석 p107

코루틴 라이브러리는 스레드의 생성과 관리를 효율성을 위해 애플리케이션 레벨의 공유 스레드풀을 제공한다. 이 공유 스레드를 Dispatchers.IO와 Dispatchers.Default가 사용하며, 공유 스레드풀에서 일부 limitedParallelism을 이용해 사용한다.

다만,  Dispatchers.IO.limitedParallelism(100)의 경우 공유스레드풀보다 더 많이 생성될 수 있다. IO가 많을 경우 추가적인 스레드를 할당할 수 있게 한 것이다. 이 방법은 기존 스레드에 영향을 주지 않고 신규로 스레드를 생성할 수 있지만, 생성에 따른 리소스로 CPU 병목이 있을 수 있다.

ㅁ 함께 보면 좋은 사이트

조세영 - 코틀린 코루틴의 정석 

반응형
Comments