Coroutine 예외 처리 2편 - Cancellation과 Exceptions, Exceptions 합치기

2023. 5. 12. 14:33·공식 문서 번역/Coroutines 공식 문서
반응형

Cancellation과 Exceptions

취소는 예외와 밀접히 연관되어 있다. Coroutine은 내부적으로 취소를 위해 CancellationException을 사용하며, 이 예외는 모든 Handler에서 무시된다. 따라서 이들은 catch블록으로부터 얻을 수 있는 추가적인 디버그 정보를 위해서만 사용되어야 한다. Coroutine이 Job.cancel을 사용해 취소될 경우 종료되지만, 부모 Coroutine의 실행을 취소하지는 않는다. 

 

val job = launch {
    val child = launch {
        try {
            delay(Long.MAX_VALUE)
        } finally {
            println("Child is cancelled")
        }
    }
    yield()
    println("Cancelling child")
    child.cancel()
    child.join()
    yield()
    println("Parent is not cancelled")
}
job.join()

📌 전체 코드는 이곳에서 확인할 수 있습니다.

 

이 코드의 출력은 다음과 같다 :

Cancelling child
Child is cancelled
Parent is not cancelled

 

만약 Coroutine이 CancellationException 말고 다른 예외를 만난다면, 그 예외로 부모 Coroutine까지 취소한다. 이 동작은 재정의할 수 없으며, 구조화된 동시성을 위해 안정적인 Coroutine 계층구조를 제공하는데 사용된다. CoroutineExceptionHandler의 구현은 자식 Coroutine들을 위해 사용되지 않는다.

 

📖  이 예에서, CoroutineExceptionHandler는 언제나 GlobalScope에서 만들어진 Coroutine에 설치된다.  main 함수의 runBlocking Scope에서 실행된 Coroutine에 예외 처리기를 설치하는 것은 의미가 없다. 설치된 CoroutineExceptionHandler가 있더라도 main Coroutine은 자식 Coroutine들이 예외로 인해 완료되면 언제나 취소되기 때문이다.

 

예외는 모든 자식 Coroutine이 종료될 때만 부모에 의해 처리되며, 다음 예제에서 확인할 수 있다 :*1

val handler = CoroutineExceptionHandler { _, exception -> 
    println("CoroutineExceptionHandler got $exception") 
}
val job = GlobalScope.launch(handler) {
    launch { // the first child
        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 { // the second child
        delay(10)
        println("Second child throws an exception")
        throw ArithmeticException()
    }
}
job.join()

📌 전체 코드는 이곳에서 확인할 수 있습니다.

 

이 코드의 출력은 다음과 같다 :

Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
CoroutineExceptionHandler got java.lang.ArithmeticException

 


📖 아래 내용은 독자의 이해를 위해 번역자가 추가한 글입니다.

*1. CoroutineExceptionHandler는 자식 Coroutine이 모두 종료된 다음에야 동작하는 것을 출력으로부터 확인할 수 있다.

 


자식 Coroutine에 CoroutineExceptionHandler을 붙이면 어떻게 될까?

만약 다음과 같은 코드를 실행한다면 어떻게 될까? 부모 Scope의 Handler를 제외하고 자식 Scope에 Handler을 추가했다.

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val job = GlobalScope.launch {
        launch { // the first child
            try {
                delay(Long.MAX_VALUE)
            } finally {
                withContext(NonCancellable) {
                    println("Children are cancelled, but exception is not handled until all children terminate")
                    delay(500)
                    println("The first child finished its non cancellable block")
                }
            }
        }
        launch(handler) { // the second child
            delay(10)
            println("Second child throws an exception")
            throw ArithmeticException()
        }
    }
    job.join()
}

 

출력은 다음과 같다.

Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
Exception in thread "DefaultDispatcher-worker-1 @coroutine#3" java.lang.ArithmeticException
	at TestKt$main$1$job$1$2.invokeSuspend(Test.kt:23)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:570)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:677)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:664)
	Suppressed: kotlinx.coroutines.DiagnosticCoroutineContextException: [CoroutineId(2), "coroutine#2":StandaloneCoroutine{Cancelling}@1f2f9c88, Dispatchers.Default]

 

부모로 전파되면 CoroutineExceptionHandler는 Exception을 잡지 못한다.

 

Context에 handler 자체를 추가할 수 있을까?

다음 질문은 Scope에 Handler을 추가하는 경우이다. 다음과 같이 GlobalScope+handler을 통해 Context에 Handler을 추가하고 시작해도 동작할지 궁금했다.

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception")
    }
    val job = (GlobalScope+handler).launch {
        launch { // the first child
            try {
                delay(Long.MAX_VALUE)
            } finally {
                withContext(NonCancellable) {
                    println("Children are cancelled, but exception is not handled until all children terminate")
                    delay(500)
                    println("The first child finished its non cancellable block")
                }
            }
        }
        launch { // the second child
            delay(10)
            println("Second child throws an exception")
            throw ArithmeticException()
        }
    }
    job.join()
}

 

실험 결과 잘 동작한다.

Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
CoroutineExceptionHandler got java.lang.ArithmeticException

 


이 글은 Coroutines 공식 문서를 번역한 글입니다.

 

원문 : Coroutine exceptions handling - Cancellation and exceptions

원문 최종 수정 :  2022년 6월 27일

 


Exceptions 합치기

만약 Coroutine의 복수의 자식들이 예외와 함께 실행에 실패한다면, 일반적인 규칙은 "첫번째 예외가 이긴다"이며, 따라서 첫 예외만 처리된다. 첫 예외 이후 생긴 모든 추가적인 예외들은 첫번째 예외에 suppressed로 붙여진다.

 

@OptIn(DelicateCoroutinesApi::class)
fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("CoroutineExceptionHandler got $exception with suppressed ${exception.suppressed.contentToString()}")
    }
    val job = GlobalScope.launch(handler) {
        launch {
            try {
                delay(Long.MAX_VALUE) // it gets cancelled when another sibling fails with IOException
            } finally {
                throw ArithmeticException() // the second exception
            }
        }
        launch {
            delay(100)
            throw IOException() // the first exception
        }
        delay(Long.MAX_VALUE)
    }
    job.join()  
}

📌 전체 코드는 이곳에서 확인할 수 있습니다.

 

📖 주의:  위 코드는 suppressed 예외를 지원하는 JDK7 버전 이상에서만 정상적으로 동작한다.

 

위 코드의 출력은 다음과 같다.*1

CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]

 

취소 예외는 투명하고 기본적으로 감싸진다.

val handler = CoroutineExceptionHandler { _, exception ->
    println("CoroutineExceptionHandler got $exception")
}
val job = GlobalScope.launch(handler) {
    val inner = launch { // all this stack of coroutines will get cancelled
        launch {
            launch {
                throw IOException() // the original exception
            }
        }
    }
    try {
        inner.join()
    } catch (e: CancellationException) {
        println("Rethrowing CancellationException with original cause")
        throw e // cancellation exception is rethrown, yet the original IOException gets to the handler  
    }
}
job.join()

📌 전체 코드는 이곳에서 확인할 수 있습니다.

 

이 코드의 출력은 다음과 같다.

Rethrowing CancellationException with original cause
CoroutineExceptionHandler got java.io.IOException

 


📖 아래 내용은 독자의 이해를 위해 번역자가 추가한 글입니다.

*1. suppressed 에 ArithmeticException이 있는 것을 확인할 수 있다.

 


이 글은 Coroutines 공식 문서를 번역한 글입니다.

 

원문 : Coroutine exceptions handling - Exceptions aggregation

원문 최종 수정 :  2022년 6월 27일

 


 

반응형

'공식 문서 번역 > Coroutines 공식 문서' 카테고리의 다른 글

Coroutine 공유 상태와 동시성 1편 - Coroutine을 여러개 실행했을 때의 문제점, Volatile은 동시성 문제를 해결하지 못한다.  (0) 2023.05.14
Coroutine 예외 처리 3편 - Supervision - SupervisorJob, SupervisionScope 사용해 예외 처리하기  (0) 2023.05.13
Coroutine 예외 처리 1편 - Exception 전파, CoroutineExceptionHandler 사용해 전파된 예외 처리하기  (0) 2023.05.11
Coroutine Channels 4편 - Buffered channels, Channel은 평등하다, Ticker channels  (0) 2023.03.08
Coroutine Channels 3편 - Fan-out과 Fan-in : Channel이 얼마나 많은 출력, 입력을 만들 수 있는지 알아보기  (0) 2023.03.07


'공식 문서 번역/Coroutines 공식 문서' 카테고리의 다른 글
  • Coroutine 공유 상태와 동시성 1편 - Coroutine을 여러개 실행했을 때의 문제점, Volatile은 동시성 문제를 해결하지 못한다.
  • Coroutine 예외 처리 3편 - Supervision - SupervisorJob, SupervisionScope 사용해 예외 처리하기
  • Coroutine 예외 처리 1편 - Exception 전파, CoroutineExceptionHandler 사용해 전파된 예외 처리하기
  • Coroutine Channels 4편 - Buffered channels, Channel은 평등하다, Ticker channels
심플코드
심플코드
프로그래밍을 어렵지 않게 풀어서 설명하는 기술 블로그
    반응형
  • 심플코드
    심플코드
    심플코드
  • 전체
    오늘
    어제
    • 분류 전체보기 (96)
      • 안드로이드를 위한 Coroutines (2)
      • Unit Testing (19)
      • GitHub Actions (0)
      • 공식 문서 번역 (35)
        • Coroutines 공식 문서 (35)
      • 알고리즘 (7)
        • Kotlin 자료구조 (0)
        • 알고리즘 (7)
        • Kotlin으로 구현하는 자료구조 (0)
      • 코딩 테스트 (0)
      • Deep Learning (0)
      • Machine Learning Math (17)
        • Linear Algebra (17)
      • ML (0)
      • Docker (15)
      • Kubernetes (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 코틀린 코루틴의 정석 책 출간 소식
  • 인기 글

  • 태그

    Coroutines Channel
    도커
    pytorch
    컨테이너
    Coroutines Flow
    TensorFlow
    코루틴
    Coroutines Context
    Coroutines
    Docker
    numpy
    unit test
    코루틴 채널
    junit
    Kotlin
    coroutine
    unit testing
    Machine Learning
    코루틴 Flow
    mockito
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
심플코드
Coroutine 예외 처리 2편 - Cancellation과 Exceptions, Exceptions 합치기
상단으로

티스토리툴바