코루틴 (Kotlin)

Exceptions handling

Exception propagation

  • 코루틴 빌더는 예외를 처리하는 방식에 따라 두 종류로 나눌 수 있다.
  • 예외를 자동으로 전파하는 빌더: launch, actor
    val job = GlobalScope.launch {
      println("Throwing exception from launch")
      throw IndexOutOfBoundsException() // 여기서 예외가 발생
                                        // Thread.defaultUncaughtExceptionHandler에 의해 에러가 출력됨
    }
    
    try {
      job.join()
      println("Reached")
    } catch (e: IndexOutOfBoundsException) {
      println("Caught IndexOutOfBoundsException") // 예외가 캐치되지 않음
    }
    
    val job = GlobalScope.launch {
      try {
        println("Throwing exception from launch")
        throw IndexOutOfBoundsException() // 여기서 예외가 발생
      } catch (e: IndexOutOfBoundsException) {
        println("Caught IndexOutOfBoundsException") // 예외가 캐치됨
      }
    }
    
  • 사용자가 예외를 처리하도록 노출하는 빌더: async, produce
    val deferred = GlobalScope.async {
      println("Throwing exception from async")
      throw ArithmeticException()
    }
    
    try {
      deferred.await() // 여기서 예외가 발생
      println("Unreached")
    } catch (e: ArithmeticException) {
      println("Caught ArithmeticException") // 예외가 캐치됨
    }
    
    val deferred = GlobalScope.async {
      try {
        println("Throwing exception from async")
        throw ArithmeticException() // 여기서 예외가 발생
      } catch (e: ArithmeticException) {
        println("Caught ArithmeticException") // 예외가 캐치됨
      }
    }
    

CoroutineExceptionHandler

  • 코루틴에서 캐치되지 않는 예외가 발생하면 CoroutineExceptionHandler가 예외를 다룬다.
  • 핸들러의 동작을 커스텀할 수도 있다:
    val handler = CoroutineExceptionHandler { _, exception ->
      println("CoroutineExceptionHandler got $exception") // 단순히 에러 메시지를 출력하는 커스텀 핸들러
    }
    
    val job = GlobalScope.launch(handler) { // 커스텀 핸들러를 전달해 코루틴을 만든다
      throw AssertionError()
    }
    
    job.join()
    

Cancellation

  • 코루틴은 동작의 중단을 위해 내부적으로 CancellationException을 사용한다.
  • CancellationException은 모든 핸들러가 무시하도록 되어있다.
  • 다른 예외와 다르게 CancellationException은 부모 코루틴까지 중단시키지 않는다.
    val job = launch {
      val child = launch {
        try {
          delay(Long.MAX_VALUE) // 대기하는 동안 부모 코루틴에게 다시 점유권이 돌아간다
        } catch (e: CancellationException) { // 핸들러가 CancellationException을 무시하기 때문에 직접 캐치한다
          println("Child is cancelled")
        }
      }
    
      yield() // 자식 코루틴에게 점유권을 넘겨준다
    
      println("Cancelling child")
      child.cancel() // 자식 코루틴을 중단한다
      child.join()
    
      yield()
    
      println("Parent is not cancelled")
    }
    
    job.join()
    
  • 부모가 예외를 다루려면 모든 자식 코루틴이 종료되어야 한다:
    val job = GlobalScope.launch {
        launch { // 첫째 자식
            try {
                delay(Long.MAX_VALUE)
            } finally {
                withContext(NonCancellable) { // 이미 취소된 코루틴에서 실행할 컨텍스트
                    println("Children are cancelled, but exception is not handled until all children terminate")
                    delay(100)
                    println("The first child finished its non cancellable block")
                }
            }
        }
    
        launch { // 둘째 자식
            delay(10)
            println("Second child throws an exception")
            throw ArithmeticException() // 둘째 자식이 예외를 던지지만 부모 코루틴이 예외를 다루지 않는다.
        }
    }
    
    job.join()
    

이 문서를 인용한 문서