관리 메뉴

피터의 개발이야기

[kotlin] 코틀린 코루틴의 정석- 빌더와 Job 본문

Programming/Kotlin

[kotlin] 코틀린 코루틴의 정석- 빌더와 Job

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

 

ㅁ 들어가며

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

 

4장 코루틴 빌더와 Job

ㅁ 코루틴 빌더

// Job 객체 생성
val job: Job = launch(Dispatchers.IO) { 
  println("job은 생성된 코루틴의 상태를 추적하고 실행과 정지를 수행할 수 된다.")
}

ㅇ runBlocking 함수와 launch 함수는 코루틴을 만들기 위한 코루틴 빌더 함수이다.

ㅇ 코루틴 빌더는 코루틴을 추상화하나 Job 객체를 통해 코루틴의 상태를 추적하고 일시 중단 및 재실행을 할 수 있다.

 

ㅁ join함수를 이용한 순차처리

  val firstJob = launch(Dispatchers.IO) {
    println("[${Thread.currentThread().name}] firtst job 시작")
    delay(100L)
    println("[${Thread.currentThread().name}] firtst job 종료")
  }

  val dummyJob = launch(Dispatchers.Default) {
    delay(1000)
    println("[${Thread.currentThread().name}] 이미 스캐줄된 dummyJob은 일시정지 되지 않는다.")
  }

  firstJob.join() // firstJob을 우선처리하도록 대기한다.

  val secondJob = launch(Dispatchers.IO) {
    println("[${Thread.currentThread().name}] secondJob 시작")
  }

/* 출력
[DefaultDispatcher-worker-1 @coroutine#2] firtst job 시작
[DefaultDispatcher-worker-1 @coroutine#2] firtst job 종료
[DefaultDispatcher-worker-1 @coroutine#4] secondJob 시작
[DefaultDispatcher-worker-1 @coroutine#3] 이미 스캐줄된 dummyJob은 일시정지 되지 않는다.
*/

ㅇ  join 함수를 호출하면 해당 코루틴이 완료될 때까지 다른 루틴의 스케줄을 수행하지 않는다.

ㅇ 기존 스케줄된 dummyJob은 일시 중단하지 않는다.

 

ㅁ joinAll, 병렬처리 순서 보장

// 구현체에는 다중으로 받은 job forEach로 join을 걸어준다.
public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() }

결국 joinAll(job1, job2)는
job1.join()
job2.join()
와 동일한 작업이다.

ㅇ 병렬처리가 필요한 다중 Job의 순서 보장은 joinAll을 이용한다.

ㅇ joinAll 함수를 사용해 복수의 코루틴이 실행 완료될 때까지 대기할 수 있다.

 

ㅁ 지연시작, CoroutineStart.LAZY

fun main() = runBlocking<Unit> {
  val batchJob: Job = launch(start = CoroutineStart.LAZY) {
    println("[${Thread.currentThread().name}] 배치작업 실행")
  }
  println("[${Thread.currentThread().name}] 메인작업 실행")
  delay(1000L) // 1초간 대기
  batchJob.start() // 코루틴 실행
}
/* 출력
[main @coroutine#1] 메인작업 실행
[main @coroutine#2] 배치작업 실행
*/

ㅇ 작업 중에 우선 순위가 낮고 오래 걸리는 배치성 업무가 있을 수 있다.

ㅇ 서비스 지연을 예방하기 위해 메인작업 이후에 실행시킬 수 있다.

ㅇ LAZY로 정의된 루틴은 start()로 실행된다.

ㅇ 참고: CoroutineStart의 다른 옵션은 [kotlin] 코틀린 코루틴의 정석 - CoroutineScope의 start 옵션에 정리함.

 

ㅁ cancel

fun main() = runBlocking<Unit> {
  val longJob: Job = launch(Dispatchers.Default) {
    repeat(10) { i ->
      delay(500L) // 500밀리초 대기
      println("${getData()} ${i}")
    }
  }
  delay(2000) // 지연
  longJob.cancel() // 코루틴 취소
}

fun getData(): String = "[${SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SS").format(System.currentTimeMillis())}]"

/*
// 결과:
[2024-05-23 01:11:13.280] 0
[2024-05-23 01:11:13.795] 1
[2024-05-23 01:11:14.301] 2
*/

ㅇ Job의 cancel()를 사용해 코루틴에 취소할 수 있다.

ㅇ cancel은 수행 중인 루틴이 바로 취소되지 않고, 코루틴의 취소 플래그가 바뀌면 취소가 된다. 

 

ㅁ cancelAndJoin

// cancel을 했지만 루틴은 지속된다.
fun main() = runBlocking<Unit> {
  val cancelJob: Job = launch(Dispatchers.Default) {
    var i = 0
    while(true) {
      println("${i++} doing")
    }
  }
  delay(2000) // 100밀리초 대기
  cancelJob.cancel() // 코루틴 취소
}
/*
// 결과:
...
17203094 doing
17203095 doing
17203096 doing
17203097 doing

종료 코드 130 (interrupted by signal 2:SIGINT)(으)로 완료된 프로세스
*/

ㅇ 취소가 되어도 루틴이 즉시 멈추지 않을 수 있다.

ㅇ cancel을 해도 코루틴이 취소를 확인할 수 없는 상태에서는 계속 실행된다.

 

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val cancelJob: Job = launch(Dispatchers.Default) {
    var i = 0
    while(true) {
      println("rest 시점에 cancel이 수행된다.")
      delay(10) // 루틴이 잠시 휴식
      repeat(10000){  // 반복작업이 길어지는 만큼 cancel의 시간도 늘어난다.
        println("${getData(System.currentTimeMillis())} ${i++} doing")
      }
    }
  }
  delay(30) // 대기
  cancelJob.cancel() // 코루틴 취소
  println("${getData(startTime)} 시작시간")
}
fun getData(time:Long): String = "[${SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SS").format(time)}]"
/*
rest 시점에 cancel이 수행된다.
[2024-05-23 02:01:08.102] 시작시간
[2024-05-23 02:01:08.120] 0 doing
[2024-05-23 02:01:08.152] 1 doing
[2024-05-23 02:01:08.152] 2 doing
~~~~~
[2024-05-23 02:01:08.223] 9996 doing
[2024-05-23 02:01:08.223] 9997 doing
[2024-05-23 02:01:08.223] 9998 doing
[2024-05-23 02:01:08.223] 9999 doing
rest 시점에 cancel이 수행된다.
*/

ㅇ 루틴이 잠시 휴식하는 경우 cancel이 정상적으로 작동하였다.

ㅇ 이처럼 cancel은 스레드의 유휴상태에 따라 작동 시간을 보장할 수 없다.

ㅇ 취소되는 시점에 다음 루틴이 실행되려면 cancelAndJoin 함수를 사용해야한다.

 

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val cancelJob: Job = launch(Dispatchers.Default) {
    var i = 0
    while(true) {
      println("rest 시점에 cancel이 수행된다.")
      delay(10) // 루틴이 잠시 휴식
      repeat(100000){  // 반복작업이 길어지는 만큼 cancel의 시간도 늘어난다.
        println("${getData(System.currentTimeMillis())} ${i++} doing")
      }
    }
  }
  delay(30)
  cancelJob.cancelAndJoin() // <<<<<<<  취소 시 순서 보장하도록 변경
  println("${getData(startTime)} 시작시간")
}
fun getData(time:Long): String = "[${SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SS").format(time)}]"

/*
~~~~~
[2024-05-23 02:07:10.176] 99993 doing
[2024-05-23 02:07:10.176] 99994 doing
[2024-05-23 02:07:10.176] 99995 doing
[2024-05-23 02:07:10.176] 99996 doing
[2024-05-23 02:07:10.176] 99997 doing
[2024-05-23 02:07:10.176] 99998 doing
[2024-05-23 02:07:10.176] 99999 doing
rest 시점에 cancel이 수행된다.
[2024-05-23 02:07:09.875] 시작시간
*/

ㅇ 시작시간 로그가 마지막에 나타났다.

ㅇ cancelJob이 취소 완료된 후에 시작시간 로그가 나타났다.

 

ㅁ cancel을 만드는 세가지 방법

ㅇ delay, yield 함수나 isActive 프로퍼티 등을 사용해 코루틴이 취소를 확인할 수 있도록 만들 수 있다.

delay, yield

fun main() = runBlocking<Unit> {
  val cancelJob: Job = launch(Dispatchers.Default) {
    while(true) {
      println("doing job")
//      delay(1)
//      yield()
    }
  }
  delay(10)
  cancelJob.cancel()
}

ㅇ delay와 yield로 cancel를 수행할 수 있다.

 

isActive

fun main() = runBlocking<Unit> {
  val cancelJob: Job = launch(Dispatchers.Default) {
    while(this.isActive) { // 
      println("doing job")
    }
  }
  delay(10)
  cancelJob.cancel()
}

ㅇ cancel을 지원하려면 장기 실행 루프에서 isActive 속성을 확인해야한다.

 

ㅁ 코루틴의 상태

코틀린 코루틴의 정석 p135

ㅇ 코루틴은 생성, 실행 중, 실행 완료 중, 실행 완료, 취소 중, 취소 완료 상태를 가진다.

 

생성(New) : 코루틴 빌러를 통해 생성된 상태.

실행중(Active) : CoroutineStart.Lazy가 아니면 자동으로 실행 상태가 된다.

실행완료(Completed) : 모든 코드가 실행 완료된 상태

취소 중(Cancelling) : Job.cancel()등을 통해 취소가 요청된 상태로 넘어가는 중

취소 완료(Cancelled) : 코루틴의 취소 확인 시점에 취소가 된 경우

 

ㅁ Job의 상태변수

Coroutine State isActive isCancelled isCompleted
New false false false
Active true false false
Completed false false true
Cancelling false true false
Cancelled false true true

ㅇ Job 객체는 isActive, isCancelled, isCompleted 프로퍼티를 통해 코루틴의 상태를 나타낸다.

 

fun main() = runBlocking<Unit> {
  val jobStateChk = launch(start = CoroutineStart.LAZY) { // 생성 상태의 Job 생성
    delay(1000)
  }

  jobStateChk.start()
  println(">> job is started!!!!")
  println("isActive >> ${jobStateChk.isActive}")
  println("isCancelled >> ${jobStateChk.isCancelled}")
  println("isCompleted >> ${jobStateChk.isCompleted}" )
  println()

  jobStateChk.cancel()
  println(">> job is cancel!!!!")
  println("isCancelled >> ${jobStateChk.isCancelled}")
  println()

  jobStateChk.start()
  joinAll(jobStateChk)
  println(">> job is done!!!!")
  println("isCompleted >> ${jobStateChk.isCompleted}" )

}
/*
>> job is started!!!!
isActive >> true
isCancelled >> false
isCompleted >> false

>> job is cancel!!!!
isCancelled >> true

>> job is done!!!!
isCompleted >> true
*/

ㅇ 생성 상태일 때 isActives = false

ㅇ 실행되면 isActives = true

ㅇ cancel 혹은 실행 완료 시 false

ㅇ isCancelled는 코루틴이 취소 중이거나 취소 완료됐을 때만 true

ㅇ isCompleted는 코루틴이 취소 완료되거나 실행 완료 됐을 때만 true

ㅇ 코루틴 라이브러리를 효율적으로 사용하기 위해서는 코루틴의 상태변화를 이해하는 것이 중요하다.

 

ㅁ 함께 보면 좋은 사이트

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

반응형
Comments