-
Coroutine exceptions handling프로그래밍/Kotlin 2022. 6. 22. 00:14반응형
- 결론
- When a thread throws an exception, a process will be killed.
- When a coroutine throws an exception, the process will be killed too?
- When coroutine throws an exception, the process will be killed too.
- When children coroutines throw an exception, children coroutines will be canceled.
- 기본적으로는 child coroutine에서 Exception이 발생하면 부모 coroutine에게 자신의 실패 상태를 전파하기 때문에 나머지 children job들이 취소된다.
- 예외 적용
- CancellationException 은 코루틴 내부적으로 무시된다.
- 전체 취소가 되지 않길 바라면, 모든 children job에 try-catch를 넣거나 CoroutineExceptionHandler, SupervisionJob을 사용해본다.
- 예외 적용
- Exception handling이 필요하면, 내부적으로 try-catch나 CoroutineExceptionHandler을 사용하고
- Child coroutine이 취소되지 않길 바라면 SupervisionJob을 사용한다.
Why use children coroutines?
더보기import kotlinx.coroutines.* fun main() = runBlocking { /* this: CoroutineScope */ launch { /* ... */ } // the same as: this.launch { /* ... */ } }
- We can say that the nested coroutine (started by launch in this example) is a child of the outer coroutine (started by runBlocking). This "parent-child" relationship works through scopes: the child coroutine is started from the scope corresponding to the parent coroutine.
It's also possible to start a new coroutine from the global scope using GlobalScope.async or GlobalScope.launch. This will create a top-level "independent" coroutine.
The mechanism providing the structure of the coroutines is called "structured concurrency". Let's see what benefits structured concurrency has over global scopes:- The scope is generally responsible for child coroutines, and their lifetime is attached to the lifetime of the scope.
- The scope can automatically cancel child coroutines if something goes wrong or if a user simply changes their mind and decides to revoke the operation.
- The scope automatically waits for completion of all the child coroutines. Therefore, if the scope corresponds to a coroutine, then the parent coroutine does not complete until all the coroutines launched in its scope are complete.
자식 코루틴들은 Scope이나 부모 코루틴에 의해서 취소 가능하다.
Cancelation exception
@OptIn(DelicateCoroutinesApi::class) @Test fun test_cancel() = runBlocking { val scope = GlobalScope val job = scope.launch { try { delay(Long.MAX_VALUE) } catch (t: Throwable) { println("exception: $t.") } finally { println("finally: canceled.") } } job.cancel() job.join() } // Outputs // 코루틴 취소 시, JobCancellationException이 던져짐. try-catch로 확인 가능. // exception: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@401f4551. // finally: canceled. @OptIn(DelicateCoroutinesApi::class) @Test fun test_cancel() = runBlocking { val scope = GlobalScope val job = scope.launch { try { delay(Long.MAX_VALUE) } finally { println("finally: canceled.") } } job.cancel() job.join() } // Outputs // 딱히 exception이 나지 않음. Process도 종료 안되고 코루틴 내부적으로 무시됨. // finally: canceled.
Exception propagation
- launch and actor
- exception propagation
- When these builders are used to create a root coroutine, builders treat exceptions as uncaught exceptions, similar to Java's Thread.uncaughtExceptionHandler.
- async and produce
- exposing them to users
- When these builders are used to create a root coroutine, builders are relying on the user to consume the final exception, for example via await or receive.
- Here we look at what happens if an exception is thrown during cancellation or multiple children of the same coroutine throw an exception.
@OptIn(DelicateCoroutinesApi::class) fun main() = runBlocking { val job = GlobalScope.launch { // root coroutine with launch println("Throwing exception from launch") throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler } job.join() println("Joined failed job") val deferred = GlobalScope.async { // root coroutine with async println("Throwing exception from async") throw ArithmeticException() // Nothing is printed, relying on user to call await } try { println("Start await") deferred.await() println("Unreached") } catch (e: ArithmeticException) { println("Caught ArithmeticException") } }
Output
Throwing exception from launch Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException at com.example.largescreen.ExampleUnitTest$exception_propagation$1$job$1.invokeSuspend(ExampleUnitTest.kt:82) 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:749) 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}@3dec09e5, Dispatchers.Default] Joined failed job Start await Throwing exception from async Caught ArithmeticException
- Thread.setDefaultUncaughtExceptionHandler()
- Global하게 쓰일 수 있는 try-catch
- Thread.setUncaughtExceptionHandler()
- Thread마다 쓰일 수 있는 try-catch
// Thread.setDefaultUncaughtExceptionHandler() 없을 때, @Test fun testException() { println("Started.") thread(true) { println("Run Thread1") throw IndexOutOfBoundsException("Thread1") } thread(true) { println("Run Thread2") throw IndexOutOfBoundsException("Thread2") } println("Finished.") } // Outputs Started. Run Thread1 Finished. Run Thread2 Exception in thread "Thread-3" java.lang.IndexOutOfBoundsException: Thread1 at com.example.largescreen.ExampleUnitTest$testException$1.invoke(ExampleUnitTest.kt:33) at com.example.largescreen.ExampleUnitTest$testException$1.invoke(ExampleUnitTest.kt:31) at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30) Exception in thread "Thread-4" java.lang.IndexOutOfBoundsException: Thread2 at com.example.largescreen.ExampleUnitTest$testException$2.invoke(ExampleUnitTest.kt:38) at com.example.largescreen.ExampleUnitTest$testException$2.invoke(ExampleUnitTest.kt:36) at kotlin.concurrent.ThreadsKt$thread$thread$1.run(Thread.kt:30) // Thread.setDefaultUncaughtExceptionHandler() 있을 때, @Test fun testException() { Thread.setDefaultUncaughtExceptionHandler { t, e -> println("$t $e") } println("Started.") thread(true) { println("Run Thread1") throw IndexOutOfBoundsException("Thread1") } thread(true) { println("Run Thread2") throw IndexOutOfBoundsException("Thread2") } println("Finished.") } // Outputs Started. Run Thread1 Thread[Thread-3,5,main] java.lang.IndexOutOfBoundsException: Thread1 Run Thread2 Thread[Thread-4,5,main] java.lang.IndexOutOfBoundsException: Thread2 Finished.
CoroutineExceptionHandler
- Thread.setUncaughtExceptionHandler()와 비슷하게 사용 가능
- It is possible to customize the default behavior of printing uncaught exceptions to the console.
- CoroutineExceptionHandler context element on a root coroutine can be used as a generic catch block for this root coroutine and all its children where custom exception handling may take place.
- It is similar to Thread.uncaughtExceptionHandler.
- You cannot recover from the exception in the CoroutineExceptionHandler. The coroutine had already completed with the corresponding exception when the handler is called. Normally, the handler is used to log the exception, show some kind of error message, terminate, and/or restart the application.
- Coroutines running in supervision scope do not propagate exceptions to their parent and are excluded from this rule. A further Supervision section of this document gives more details.
- CoroutineExceptionHandler is invoked only on uncaught exceptions — exceptions that were not handled in any other way. In particular, all children coroutines (coroutines created in the context of another Job) delegate handling of their exceptions to their parent coroutine, which also delegates to the parent, and so on until the root, so the CoroutineExceptionHandler installed in their context is never used. (=propagation)
- In addition to that, async builder always catches all exceptions and represents them in the resulting Deferred object, so its CoroutineExceptionHandler has no effect either.
val handler = CoroutineExceptionHandler { _, exception -> println("CoroutineExceptionHandler got $exception") } val job = GlobalScope.launch(handler) { // root coroutine, running in GlobalScope throw AssertionError() } val deferred = GlobalScope.async(handler) { // also root, but async instead of launch throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await() } joinAll(job, deferred)
Outputs
CoroutineExceptionHandler got java.lang.AssertionError
try-catch문이 있으면 CoroutineExceptionHandler는 사용되지 않는다.
val handler = CoroutineExceptionHandler { _, exception -> println("CoroutineExceptionHandler got $exception") } val job = GlobalScope.launch(handler) { try { throw AssertionError() } catch (ex: Throwable) { println("Try-Catch $ex") } } job.join() // Outputs Try-Catch java.lang.AssertionError
child coroutine과 root coroutine에 CoroutineExceptionHandler가 있으면 root의 것이 사용된다.
val childHandler = CoroutineExceptionHandler { _, exception -> println("CoroutineExceptionHandler for child got $exception") } val job = GlobalScope.launch { launch(childHandler) { throw AssertionError("For child") } } job.join()
단, child coroutine가 단독으로 CoroutineExceptionHandler를 사용할 수 없다.
val handler = CoroutineExceptionHandler { _, exception -> println("CoroutineExceptionHandler got $exception") } val job = GlobalScope.launch { launch(handler) { throw AssertionError() } } job.join() // Output Exception in thread "DefaultDispatcher-worker-2 @coroutine#3" java.lang.AssertionError at com.example.largescreen.ExampleUnitTest$childFailed2$1$job$1$1.invokeSuspend(ExampleUnitTest.kt:402) 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:749) 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}@76e5dd07, Dispatchers.Default]
CoroutineExceptionHandler가 Exception propagation을 막진 못한다.
val rootHandler = CoroutineExceptionHandler { _, exception -> println("CoroutineExceptionHandler for root got $exception") } val job = GlobalScope.launch(rootHandler) { val firstChildJob = launch { delay(50L) throw AssertionError("First child job exception.") } val secondChildJob = launch { println("Start second child job.") try { firstChildJob.join() delay(Long.MAX_VALUE) } catch (e: CancellationException) { println("Canceled second child job $e") } finally { println("Finished second child job.") } } firstChildJob.join() secondChildJob.join() } job.join() // Output Start second child job. Canceled second child job kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job="coroutine#2":StandaloneCoroutine{Cancelling}@5f8ded00 Finished second child job. CoroutineExceptionHandler for root got java.lang.AssertionError: First child job exception.
Cancellation and exceptions
- Cancellation is closely related to exceptions.
- Coroutines internally use CancellationException for cancellation, these exceptions are ignored by all handlers, so they should be used only as the source of additional debug information, which can be obtained by catch block.
- When a coroutine is cancelled using Job.cancel, it terminates, but it does not cancel its parent.
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()
Output
Cancelling child Child is cancelled Parent is not cancelled
CancellationException으로 부모 코루틴이 취소되지는 않는다. 하지만, CancellationException이 아닌 다른 Exception이면 얘기가 다르다.
= CancellationException이 propagation까지 막진 않는다.
Children coroutine exception propagation 상세
If a coroutine encounters an exception other than CancellationException, it cancels its parent with that exception. This behaviour cannot be overridden and is used to provide stable coroutines hierarchies for structured concurrency. CoroutineExceptionHandler implementation is not used for child coroutines.
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() } } println("--Children are running--") job.join() println("--Children done--")
Output
--Children are running-- 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 --Children done--
Exceptions aggregation
When multiple children of a coroutine fail with an exception, the general rule is "the first exception wins", so the first exception gets handled. All additional exceptions that happen after the first one are attached to the first exception as suppressed ones.
import kotlinx.coroutines.* import java.io.* @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() }
Note: This above code will work properly only on JDK7+ that supports suppressed exceptions.
Output (Note that this mechanism currently only works on Java version 1.7+.)CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]
Cancellation exceptions are transparent and are unwrapped by default:
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()
Output
Rethrowing CancellationException with original cause CoroutineExceptionHandler got java.io.IOException
Child 중 하나는 CancellationException이 호출될 것이기 때문에 catch 되고 throw 되지만, handler는 CancellationException이 아닌 IOException을 받는다.
Supervision
Exception이 발생하면 cancellation이 코루틴 계층 전체적으로 양방향(부모와 나머지 자식들)으로 전파(bidirectional relationship propagation)되는 것을 확인했다.
As we have studied before, cancellation is a bidirectional relationship propagating through the whole hierarchy of coroutines. Let us take a look at the case when unidirectional cancellation is required.
A good example of such a requirement is a UI component with the job defined in its scope. If any of the UI's child tasks have failed, it is not always necessary to cancel (effectively kill) the whole UI component, but if the UI component is destroyed (and its job is cancelled), then it is necessary to cancel all child jobs as their results are no longer needed. (onDestroy() 시, 단방향으로 다 취소)
Another example is a server process that spawns multiple child jobs and needs to supervise their execution, tracking their failures and only restarting the failed ones. (일부 코루틴 실패 감시 후 그 것만 다시 시작)
SupervisorJob
The SupervisorJob can be used for these purposes. It is similar to a regular Job with the only exception that cancellation is propagated only downwards(부모에게 전달하지 않음).
This can easily be demonstrated using the following example:
val supervisor = SupervisorJob() 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 { _, _ -> }) { println("The first child is failing") throw AssertionError("The 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("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active") try { delay(Long.MAX_VALUE) } finally { // But cancellation of the supervisor is propagated println("The second child is cancelled because the supervisor was cancelled") } } // wait until the first child fails & completes firstChild.join() println("Cancelling the supervisor") supervisor.cancel() secondChild.join() }
Output
The first child is failing The first child is cancelled: true, but the second one is still active Cancelling the supervisor The second child is cancelled because the supervisor was cancelled
단, Exception을 propagation하진 않지만 Exception의 try-catch까진 해주진 않는다.
SupervisionScope
CoroutineScope + SupervisorJob을 생성하는 것과 동일하다. (supervisionScope : Creates a CoroutineScope with SupervisorJob and calls the specified suspend block with this scope.)
Instead of coroutineScope, we can use supervisorScope for scoped concurrency. It propagates the cancellation in one direction only and cancels all its children only if it failed itself. It also waits for all children before completion just like coroutineScope does.
try { supervisorScope { val child = launch { try { println("The child is sleeping") delay(Long.MAX_VALUE) } finally { println("The child is cancelled") } } // Give our child a chance to execute and print using yield yield() println("Throwing an exception from the scope") throw AssertionError() } } catch(e: AssertionError) { println("Caught an assertion error") }
Output
The child is sleeping Throwing an exception from the scope The child is cancelled Caught an assertion error
supervision scope vs supservision job
CoroutineScope + SupervisionJob을 생성하는 것과 동일하다. (supervisionScope : Creates a CoroutineScope with SupervisorJob and calls the specified suspend block with this scope.)
Exceptions in supervised coroutines
Another crucial difference between regular and supervisor jobs is exception handling. Every child should handle its exceptions by itself via the exception handling mechanism. This difference comes from the fact that child's failure does not propagate to the parent. It means that coroutines launched directly inside the supervisorScope do use the CoroutineExceptionHandler that is installed in their scope in the same way as root coroutines do (see the CoroutineExceptionHandler section for details).
val handler = CoroutineExceptionHandler { _, exception -> println("CoroutineExceptionHandler got $exception") } supervisorScope { val child = launch(handler) { println("The child throws an exception") throw AssertionError() } println("The scope is completing") } println("The scope is completed")
Output
The scope is completing The child throws an exception CoroutineExceptionHandler got java.lang.AssertionError The scope is completed
children coroutine 구조 어디에 쓸까?
child coroutine은 같은 scope, context 안에서 동시에 처리될 수 있음. async 빌더를 쓰면 값 리턴이 필요하지만 launch를 사용하면 결과 값이 필요 없고 두 개의 child coroutine이 완료 돼야 UI 변경 되는 일? 둘 중 하나가 에러가 발생하면 UI 변경이 취소 되어야 하는 일?
https://medium.com/androiddevelopers/coroutines-first-things-first-e6187bf3bb21
https://medium.com/androiddevelopers/exceptions-in-coroutines-ce8da1ec060c
https://thdev.tech/kotlin/2019/04/30/Coroutines-Job-Exception/
반응형'프로그래밍 > Kotlin' 카테고리의 다른 글
[Kotlin new features] Sealed interface, Data object, Enum entries (0) 2023.09.16 Testing Kotlin coroutines on Android (0) 2022.07.19 Coroutine - Cancellation and timeouts (0) 2022.05.09 Making our Android Studio Apps Reactive with UI Components & Redux (0) 2020.11.08 Kotlin multiplatform 프로젝트를 생성해보자 (0) 2020.09.01 - 결론