이 글은 아래 링크의 내용을 기반으로 하여 설명합니다.
https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md
또한 예제에서 로그 print시 println과 안드로이드의 Log.e()를 혼용합니다.
코루틴에서 Exception은 자식, 부모 양방향으로 전부 전달됩니다.
UI component 같은곳에서 하나의 Job을 사용하면 UI 자체를 destroy하거나 화면을 떠나는 경우 모든 자식들을 취소 시킬 수 있습니다.
다만, 자식중 하나가 실패되면 모든 UI component가 취소되는 상황도 같이 일어납니다.
Supervision job
이렇게 한방향으로만 취소를 전달하기 위한 방법으로 SupervisorJob이 있습니다.
SupervisorJob의 경우 아래 방향으로만 취소를 전파시킵니다
runBlocking {
val supervisor = SupervisorJob()
try {
with(CoroutineScope(coroutineContext + supervisor)) {
// launch the first child -- its exception is ignored for this example (don't do this in practice!)
val firstChild = launch(CoroutineExceptionHandler { _, exception -> println("caught $exception")}) {
println("First child is failing")
throw AssertionError("First child is cancelled")
}
// launch the second child
val secondChild = launch {
firstChild.join()
// Cancellation of the first child is not propagated to the second child
println("First child is cancelled: ${firstChild.isCancelled}, but second one is still active"
)
try {
delay(Long.MAX_VALUE)
} finally {
// But cancellation of the supervisor is propagated
println("Second child is cancelled because supervisor is cancelled")
}
}
// wait until the first child fails & completes
firstChild.join()
println("Cancelling supervisor")
supervisor.cancel()
secondChild.join()
}
} catch (e: CancellationException) {
println("CoroutineScope is cancelled!")
}
}
Supervision Scope
fun main() = runBlocking {
try {
supervisorScope {
val child = launch {
try {
println("Child is sleeping")
delay(Long.MAX_VALUE)
} finally {
println("Child is cancelled")
}
}
// Give our child a chance to execute and print using yield
yield()
println("Throwing exception from scope")
throw AssertionError()
}
} catch(e: AssertionError) {
println("Caught assertion error")
}
}
runBlocking {
try {
supervisorScope {
val child = launch {
try {
Log.e(TAG, "Child is sleeping")
delay(Long.MAX_VALUE)
} finally {
Log.e(TAG, "Child is cancelled")
}
}
val child2 = launch {
Log.e(TAG, "Throwing exception from scope")
throw AssertionError()
}
}
} catch (e: AssertionError) {
Log.e(TAG, "Caught assertion error")
}
}
위 예제와 다른건 throw를 또다른 launch 안에서 했다는 겁니다.
이런 경우 아래와 같이 찍힙니다.
Child is sleeping
Throwing exception from scope
Exception in thread "main" java.lang.AssertionError
위 예제에서는 AssertionError을 처리하지 못하고 Exception이 그대로 발생되면서 process가 중지 됩니다.
SuperviorScope인 경우 부모로 실패를 전달하지 못하기 때문에 해당 자식이 반드시 Exception을 처리해야만 합니다.
위 예제가 coroutienScope이었다면 try-catch가 동작하면서 예제의 모든 로그(네개)가 다 찍힙니다.
Exceptions in supervised coroutines
fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Caught $exception")
}
try {
supervisorScope {
val child = launch(handler) {
println("Child throws an exception")
throw AssertionError()
}
println("Scope is completing")
}
println("Scope is completed")
} catch(e: Exception) {
Log.e(TAG, "Exception happen!")
}
}
Summary
- coroutineScope
- Exception이 발생시 자식 Coroutine을 모두 취소후 부모로 Exception이 전달 시킨다.
- 부모로 전달된 Exception은 try-catch으로 처리해야 한다. exception handler로는 처리할 수 없다.
- 단!! try-catch는 해당 scope 내부에 존재하면 exception을 handling 할 수 없다.
- 실패가 부모 scope으로 전달되는 특성때문으로, try-catch를 하는 부분은 exception이 발생한 곳과 다른 coroutineScope이어야 한다.
- supervisorScope
- SupervisorScope 자체에서 Exception이 발생하면 자식 Coroutine을 모두 취소시킨후 부모로 Exception을 전달 시킨다.
- SupervisorScope는 param으로 Handler를 설정할 수 없기 때문이다.
- SupervisorScope의 자식 coroutine에서 Exception이 발생하는경우 자식 스스로 Exception을 처리해야 한다.
- 자식이 Handler를 달든..자식 내부에서 try-catch를 하든...
- 이는 부모로 실패를 전달하지 않는 특성에 기인한다.
- 만약 자식이 Handler 없이 exception을 일으키면 외부에서는 try-catch로는 이 exception을 handling 할 수 없다!!!
- 외부에서 CoroutineExceptionHandler를 이용해서 처리해야 한다.
- 여러개의 coroutine builder 중첩되어 안쪽에서 excpetion이 발생하는 경우 handler는 가장 바깥쪽에 위치해야만 exception을 전달받을 수 있다.
supervisorScope { launch(handler) { launch { launch { throw IOException } } } }
- supervisorJob
- handler는 supervisorJob과 동일한 위치에 존재하거나 상위에 존재해야만 정상적으로 동작한다.
supervisorJob이 다른 위치 (launch)에 존재하면 exception이 위로 전파되지는 않으나, handler는 동작하지 않는다.
val supervisorJob = SupervisorJob() // case #1: supervisorJob으로 인해 process가 죽지않고 exception은 위로 전달해 준다. // 따라서 정상적으로 handler가 동작한다. launch(handler) { launch { launch(supervisorJob) { throw IOException() } } } // case #2: supervisorJob으로 인해 process가 죽지않고 exception은 handler에서 바로 처리한다. launch { launch { launch(supervisorJob + handler) { throw IOException() } } } // case #3: exception이 발생한 상태에서는 supervisorJob과는 상관 없으므로 handler는 동작하지 못한다. // 단 supervisorJob까지 exception이 올라오면 더이상 위로 올라가는걸 막는다. launch(supervisorJob) { launch { launch(handler) { throw IOException() } } }
'개발이야기 > Kotlin' 카테고리의 다른 글
[Kotlin] 코틀린 - 코루틴#8 - 동기화 제어 (0) | 2018.12.26 |
---|---|
[Kotlin] 코틀린 - 코루틴#7 - Channels (0) | 2018.12.21 |
[Kotlin] 코틀린 - 코루틴#5 - exception (0) | 2018.12.12 |
[Kotlin] 코틀린 - 코루틴#4 - context와 dispatchers (0) | 2018.12.09 |
[Kotlin] 코틀린-코루틴#3 - suspending function의 구성 (0) | 2018.12.06 |