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일