코틀린/Kotlin Coroutine

[Kotlin][Coroutine] CoroutineBuilder와 Job

sxunea 2024. 6. 25. 22:39

 

본 글은 조세영님의 <코틀린 코루틴의 정석>을 읽고 이를 바탕으로 작성되었습니다.

 

 

 

 

앞선 글에서 코루틴을 사용하는 이유와 생성, 코루틴 디스패처에 대해 알아보았다면, 본격적으로 코루틴의 처리, 취소에 대해 알아보자.

 

 

코루틴 빌더 함수

  • launch
  • runBlocking

→ 모든 코루틴 빌더 함수는 코루틴을 생성하고 코루틴을 추상화한 Job객체를 생성한다

 

 

코루틴의 순차 처리

코루틴을 사용하다 보면 그 순서를 정해서 순차적으로 처리해야하는 일이 빈번하게 발생한다. 예를 들어 갱신된 토큰값을 받아온다던가, 네트워크 연결 후 결과값을 받아온다던가, 데이터 베이스에서 작업을 순차적으로 처리해야한다던가 등이다.

 

그러면 코루틴은 그 순서를 어떤식으로 보장해서 처리할까 ?

 

 

join을 사용하는

  • 함수를 호출한 코루틴은 join의 대상이 된 코루틴(job)이 완료될때까지 일시 중단된다
  • 따라서 join은 코루틴 등 일시 중단이 가능한 지점에서만 호출될 수 있다
  • 이때 join은 join을 호출한 코루틴만 중단시키는거지 이미 실행중인 다른 코루틴을 중단시키지는 않는다
fun main() = runBlocking<Unit> {
  val updateTokenJob = launch(Dispatchers.IO) {
    println("[${Thread.currentThread().name}] 토큰 업데이트 시작")
    delay(100L)
    println("[${Thread.currentThread().name}] 토큰 업데이트 완료")
  }
  updateTokenJob.join() // updateTokenJob이 완료될 때까지 일시 중단
  val networkCallJob = launch(Dispatchers.IO) {
    println("[${Thread.currentThread().name}] 네트워크 요청")
  }
}
/*
// 결과:
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 완료
[DefaultDispatcher-worker-1 @coroutine#3] 네트워크 요청
*/

 

 

joinAll을 사용하는

  • 복수의 코루틴의 실행이 모두 끝날 때까지 호출부의 코루틴을 일시 중단시킨다
public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach { it.join() }

이렇게 코루틴을 순차처리하는 방법을 알아봤다면 원하는 시점에 코루틴을 실행시키는 방법, 즉 코루틴을 지연 시작하는 법도 알아보자.

 

 

 

코루틴의 지연시작

CoroutineStart.LAZY를 이용해 코루틴 지연하기

  • 코루틴을 생성해놓고 나중에 지연 시작하게 하고 싶은 경우 이를 사용한다.
  • launch 함수의 start 인자로 CoroutineStart.LAZY를 넘겨서 옵션을 적용한다
  • 생성 후 대기 상태에 놓이고, 실행을 요청해야 시작된다
  • LAZY로 launch해 생성하고, .start()를 통해 실행시켜야 시작된다
fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val lazyJob: Job = launch(start = CoroutineStart.LAZY) {
    println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 지연 실행")
  }
  delay(1000L) 
  lazyJob.start() // lazyJob 코루틴 실행하게 ! (결국 1초 뒤 시작 -> 직접 실행 요청 필요)
}
/*
// 결과:
[main @coroutine#2][지난 시간: 1014ms] 지연 실행
*/

 

 

 

코루틴 취소하기

코루틴을 생성하고, 처리하고 시점을 조절한다면 당연히 취소하는 방법도 있을 것이다.

 

 

cancel 사용해 코루틴 취소

  • 원하는 코루틴에 .cancel()을 통해 특정 시점에 취소할 수 있다.
certainJob.cancel()

그러면 특정 코루틴을 취소하고 그 이후 바로 어떤 작업을 실행시켜야 하는 상황이 있다면, cancel() 후 launch()하면 제대로 동작할까 ?

  • 아니다. cancel()하면 코루틴은 즉시 취소되는 것이 아니라 job객체에 취소 확인용 플래그를 ‘취소 요청됨’으로 변경함으로서 취소해야함을 알린다.
  • 따라서 cancel()이 호출된다고 즉시 코루틴이 취소될 것임을 보장할 수 없다.
  • 그러므로 cancel() 후 launch() 의 실행순서가 코드작성 순서대로임을 보장할 수 없다.

이렇게 취소 후 실행의 순서를 보장해 순차처리하기 위해 cancelAndJoin이 사용된다.

 

 

cancelAndJoin 사용해 순차처리

  • cancelAndJoin의 대상이된 코루틴이 취소될 때 까지 호출부의 코루틴이 중단된다.
  • 따라서 아래 코드를 보면 executeAfterJobCancelled()는 longJob 코루틴이 취소 완료된 후에 실행될 수 있다.
fun main() = runBlocking<Unit> {
  val longJob: Job = launch(Dispatchers.Default) { 
  }
  longJob.cancelAndJoin() // longJob이 취소될 때까지 runBlocking 코루틴 일시 중단
  executeAfterJobCancelled()
}

fun executeAfterJobCancelledfun executeAfterJobCancelled() {
() {
  // 이 함수는 longJob이 취소된 후 실행될 것
}

 

 

 

앞서 잠깐 언급했는데 cancel()이 호출되면 취소 확인용 플래그를 ‘취소 요청됨’으로 변경한다고 했다.

결국 코루틴이 취소 완료가 되려면 코루틴이 이 플래그를 확인해야 가능하다는 것이다.

 

그러면 코루틴이 이 취소 플래그를 확인하는 시점은 언제일까 ?

 

바로 일시 중단 시점이나 실행을 대기 하는 시점이다. 그리고 코루틴이 취소되도록 만드는, 즉 취소를 확인하는 방법에는 세가지가 있다.

 

 

 

코루틴의 취소 확인 - delay, yield, isActive

 

 

delay를 이용한 취소 확인

취소 확인은 일시 중단 시점, 실행 대기 시점 이 두가지에서 할 수 있다.

그렇다면 delay에서는 어떻게 가능할까? 답은 delay의 내부코드에 있다

public suspend fun delay(timeMillis: Long) {
    if (timeMillis <= 0) return // don't delay
    return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
        // if timeMillis == Long.MAX_VALUE then just wait forever like awaitCancellation, don't schedule.
        if (timeMillis < Long.MAX_VALUE) {
            cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
        }
    }
}
  • delay는 suspend함수, 즉 일시 중단 함수로 구현되어있다.
  • 따라서 delay되는 동안 일시 중단되므로 이때 코루틴의 취소를 확인할 수 있다
  • 하지만 이 방법은 while문이 반복될 때마다 작업을 강제로 중단시키므로 효율적이지 않고, 프로그램의 성능 또한 저하시킨다

 

 

yield를 이용한 취소 확인

  • yield()는 자신이 사용하던 스레드를 양보한다.
  • 스레드 사용 양보는 곧 스레드 사용의 중단이므로 일시 중단 상태에 들어간다. 따라서 이때 취소를 확인할 수 있다
  • 하지만 아무리 코루틴이 경량 스레드라고 하더라도 매번 일시 중단되는 것은 비효율적인 작업일 수 있다

 

CoroutineScope.isActive를 사용한 취소 확인

  • coroutineScope는 코루틴이 활성화됐는지 확인할 수 있는 Boolean 타입의 프로퍼티인 isActive를 제공한다.
  • 따라서 while문의 인자로 IsActive를 넣어 취소 확인 코드를 넣어주면 코루틴이 잠시 멈추거나 양보하지 않으면서도 취소 확인을 할 수 있어서 효율적이다 !

 

 

결론적으로, 코루틴의 취소는 일시 중단 시점 / 실행 대기 시점이 생겨야 가능하다는 것을 기억하고, 이 시점이 없다면 위의 세가지 방법을 통해 취소를 확인해주어 코루틴을 취소할 수 있게 만들어주어야 한다.

 

 

 

코루틴의 상태와 job상태변수

 

 

코루틴의 상태 

코루틴은 다음과 같이 6가지 상태를 가질 수 있다.

 

생성

  • launch, runBlocking으로 생성 → 생성하면 바로 실행중으로 넘어간다
  • 바로 실행안하게 하려면 앞서 언급한 LAZY로 지연 시작시키면된다 


실행중

  • 실제 실행 중 + 일시 중단 상태 모두


취소중

  • cancel()을 통해 취소를 요청했을 경우


취소완료

  • 취소 확인 시점에 취소가 확인된 경우


실행완료

  • 코루틴의 모든 코드가 실행 완료된 경우
  • 이러한 상태들을 나타내기 위해 코루틴은 외부로 공개하는 3가지 boolean 상태 변수를 가진다

 

코루틴의 상태 변수

 

isActive

  • 코루틴의 활성화 여부 → 실행 중, 취소 요청

isCancelled

  • 코루틴의 취소 요청됐는지 여부 → 취소 요청, 취소 완료

isCompleted

  • 코루틴의 실행 완료 여부 → 실행 완료, 취소 완료

 

 

fun main() = runBlocking<Unit> {
  val job: Job = launch {
    delay(1000L) // 1초간 대기
  }
  delay(2000L) // 2초간 대기
  printJobState(job)
}
  • 1초 동안 실행되는 코루틴인데 이를 2초 동안 대기했으니까 이미 실행 완료 → 따라서 isCompleted만 true

 

 

이를 코루틴의 상태별로 job의 상태를 체크해 정리하면 다음과 같다. 

상태 isActive isCancelled isCompleted
생성 false false false
실행중 true false false
실행완료 false false true
취소중 false true false
취소완료 false true true

 

 

 

 

이번 포스트서는 코루틴의 처리와 시작, 취소에 대해 알아보았다. 또, 코루틴과 job의 상태에 대해 잘 알아두고 코루틴 내부에서 어떤 상태 전이가  일어나는지 제대로 알면 코루틴의 제대로 된 활용에 도움이 될 것 같다.