Exception propagation
-
Coroutine builder 는 2가지 flavor 가 있다.
exception 을 자동으로 전파하는 launch, actor류. 그리고 user 에게 노출되는 async, produce 류.
자동 전파하는 케이스는 Java 의 uncaughtExceptionHandler 와 비슷하게 unhandled exception 이다.
반면에 user 에게 노출되는 경우는 user 가 exception 을 직접 handle 할 수 있다. (돼왕: try-catch 할 수 있다.) 대표적 api 는 await 와 receive 가 있다.
-
runBlocking{
val job = GlobalScope.launch{
println(“Throwing exception from launch”)
throw IndexOutOfBoundsException()
}
job.join()
println(“Joined failed job”)
val deferred = GlobalScope.async{
println(“Throwing exception from async”)
throw ArithmeticException()
}
try{
deferred.await()
println(“Unreached”)
} catch(e:ArithmeticException){
println(“Caught ArithmeticException”)
}
}
// Throwing exception from launch
// Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
// Joined failed job
// Throwing exception from async
// Caught ArithmeticException
돼왕 : 사실 위의 예제는 defaultUncaughtExceptionHandler 가 장착되어 있을 때의 결과이며, 그것이 없을 때 crash 가 발생한다.
CoroutineExceptionHandler
-
GlobalScope 가 모든 exception 을 print 하는 것을 원치 않는다면, CoroutineExceptionHandler 를 context element 로 지정해주면 된다.
이 녀석은 Thread.uncaughtExceptionHandler 와 비슷하다.
-
JVM 에서 모든 coroutine 에 대한 global exception handler 를 재정의 할 수 있다.
이는 ServiceLoader 에 CoroutineExceptionHandler 를 등록하면 할 수 있다.
Global exception handler 는 Thread.defaultUncaughtExceptionHandler 와 비슷하다.
Android 에서는 uncaughtExceptionPreHandler 가 global coroutine exception handler 로 등록되어 있고, 이 녀석은 더 선행하여 불리는 exceptionHandler 가 없으면 불리게 된다.
CoroutineExceptionHandler 는 user 가 handle 하지 않는 exception 들에 대해서만 작동한다.
그래서 아래와 같이 async builder 를 사용하면 영향력이 없다.
runBlocking{
val handler = CoroutineExceptionHandler { _, exception ->
println(“Caught $exception”)
}
val job = GlobalScope.launch(handler){
throw AssertionError()
}
val deferred = GlobalScope.async(handler){
throw ArithmeticException()
}
joinAll(job, deferred)
}
// Caught java.lang.AssertionError
Cancellation and exceptions
-
Coroutine 은 내부적으로 취소시 CancellationException 을 사용한다.
이 exception 들은 모든 handler 에 의해 무시된다.(돼왕 : crash 를 내지 않는다.) 그래서 이들은 debug info 로만 사용되며, catch 로 잡을 수도 있긴 하다.
coroutine 은 Job.cancel 를 통해 취소하면, parent 를 cancel 시키지 않고 job 에 종속된 그 녀석만 종료한다.
-
coroutine 이 CancellationException 이외의 exception 을 마딱뜨리면, parent 를 exception 과 함께 종료시킨다.
이 동작은 override 되어 변경될 수 없고, CoroutineExceptionHandler 구현에 의존하지 않는 structured concurrency hierarchy 를 제공한다.
exception 은 모든 children 이 terminate 된 후에 parent 에 의해 처리된다.
-
runBlocking{
val handler = CoroutineExceptionHandler{ _, exception ->
println(“Caught $exception”)
}
val job = GlobalScope.launch(handler){
launch{
try{
delay(Long.MAX_VALUE)
} finally{
withContext(NonCancellable){
println(“Children are cancelled, but exception is no handled until all children terminated”)
delay(100L)
println(“The first child finished its non cancellable block”)
}
}
}
launch{
delay(10L)
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
// Caught ArithmeticException
위 예제에서 CoroutineExceptionHandler 는 GlobalScope 안에서 생성된 녀석에 항상 install 되어 있다.
그래서 main runBlocking scope 에서 launch 되는 coroutine 에 exception handler 를 설치하는 것은 큰 의미가 없다.
왜냐면 main coroutine 은 installed handler 에 상관없이 child 가 exception 으로 종료될 때 항상 cancel 되기 때문이다.
Exceptions aggregation
-
여러개의 children coroutine 에서 exception 을 던지면 어떻게 될까?
일반적인 규칙은 "처음 던져진 exception 이 이긴다” 이다.
그러나 이 경우 exception 이 유실될 수 있다.
-
runBlocking{
val handler = CoroutineExceptionHandler { _, exception ->
println(“Caught $exception with suppressed ${exception.suppressed.contentToString()}”)
}
val job = GloablScope.launch(handler){
launch{
try{
delay(Long.MAX_VALUE)
} finally{
throw ArithmeticException()
}
}
launch{
delay(100L)
throw IOException()
}
delay(Long.MAX_VALUE)
}
job.join()
}
// Caught java.io.IOException with suppressed [java.lang.ArithmeticException]
suppressed 는 JDK7 이상에서 작동한다.
-
Cancellation exception 은 기본적으로 투명하고 wrap 되지 않는다.
runBlocking{
val handler = CoroutineExceptionHandler { _, exception ->
println(“Caught original $exception”)
}
val job = GlobalScope.launch(handler){
val inner = launch{
launch{
launch{
throw IOException()
}
}
}
try{
inner.join()
} catch(e:CancellationException){
println(“Rethrowing CancellationException with original cause”)
throw e
}
}
job.join()
}
// Rethrowing CancellationException with original cause
// Caught original java.io.IOException
Supervision
-
취소는 전체 coroutine hierarchy 에 propagation 되며 양방향의 관계를 가지고 있다.
그러나 한쪽방향의 취소가 필요하다면?
이런 요구사항의 좋은 예는 scope 내에서 job 을 가진 UI component 이다.
만약 UI 의 child task 가 실패한다면, 모든 UI component 를 취소시킬 필요는 없다.
그러나 해당 UI component 가 destroy 된다면, 모든 child job 들의 결과는 필요 없으므로 다 취소시키는 것이 맞다.
다른 예제는 서버 process 로 여러 child job 을 생성한 경우이며, 그들의 작업을 관리할 필요가 있을 때이다.
그들이 실패했다면, 실패한 모든 child job 을 재시도시킬 수 있다.
** Supervision job
-
SupervisorJob 이 이런 목적으로 사용될 수 있다.
이 녀석은 일반 Job 과 비슷하지만, 취소가 오직 아래 방향으로만 전파된다.
fun main() = runBlocking{
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)){
// supervisor 가 없었다면 이 exceptionHandler 에 걸리지도 않는다.
// supervisor 가 있기 떄문에 여기서 exceptionHandler 로 자체처리 해야 crash 가 나지 않는다.
val firstChild = launch(CoroutineExceptionHandler { _, _ -> }){
println("First child is failing")
throw AssertionError("First child is cancelled")
}
val secondChild = launch {
firstChild.join()
println("First child is cancelled: ${firstChild.isCancelled}, but second one is still active")
try{
delay(Long.MAX_VALUE)
} finally{
println("Second child is cancelled because supervisor is cancelled")
}
}
firstChild.join()
println("Cancelling supervisor")
supervisor.cancel()
secondChild.join()
}
}
// First child is failing
// First child is cancelled: true, but second one is still active
// Cancelling supervisor
// Second child is cancelled because supervisor is cancelled
* Supervision scope
-
scoped 병렬을 위해 supervisorScope 가 coroutineScope 대신 사용될 수 있다.
이 녀석은 한쪽 방향으로만 취소를 전달한다. 스스로가 취소되었을 때 child 들에게 취소를 전달한다.
그리고 이 녀석은 coroutineScope 가 그랬듯이 child 의 완료를 기다린다.
fun main() = runBlocking{
try{
supervisorScope{
val child = launch{
try{
println("Child is sleeping")
delay(Long.MAX_VALUE)
} finally{
println("Child is cancelled")
}
}
yield()
println("Throwing exception from scope")
throw AssertionError()
}
} catch(e:AssertionError){
println("Caught assertion error")
}
}
// Child is sleeping
// Throwing exception from scope
// Child is cancelled
// Caught assertion error
** Exceptions in supervised coroutines
-
supervisor job 과 일반 job 의 다른 특별한 특징은 exception 처리에 있다.
모든 child 는 스스로 exception 을 처리해야 한다.
이 차이는 child 의 실패가 parent 로 전파되지 않기 때문이다.
fun main() = runBlocking{
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
supervisorScope{
val child = launch(handler){
println("Child throws an exception")
throw AssertionError()
}
println("Scope is completing")
}
println("Scope is completed")
}
// Scope is completing
// Child throws an exception
// Caught java.lang.AssertionError
// Scope is completed
참고 : https://kotlinlang.org/docs/exception-handling.html
actor, android uncaughtexceptionprehandler, Async, cancellation and exceptions, cancellationexception, children terminate, context element, coroutine exception, coroutine exception handling, coroutineexceptionhandler, exception aggregation, Exception Handling, Exception Propagation, exception.suppressed, global coroutine exception handler, globalscope, globalscope default exceptionhandler, jdk version, launch, main coroutine, parent propagation, produce, serviceloader, uncaughtexceptionhandler, unhandled exception
'프로그래밍 놀이터 > Kotlin, Coroutine' 카테고리의 다른 글
[coroutine] exception 을 이해해보자 #2 ( in global scope ) (0) | 2019.03.06 |
---|---|
[coroutine] exception 을 이해해보자 #1 ( in my coroutine scope ) (0) | 2019.03.05 |
[Coroutine] Coroutine context and dispatchers (4) | 2019.02.08 |
[android] AsyncTask 를 Coroutine 으로 바꿔본 후기 (0) | 2019.02.07 |
[도서 정리] Android Development with Kotlin - Delegates (0) | 2018.12.17 |
댓글