【Coroutines】Cascade Mechanism of Exception and Cancellation
[ Coroutines ] Cascade Mechanism of Exception and Cancellation
·
文章目录
there are many factors can decide how exception and cancellation affect coroutine flows
i do not want to talk about too much theroy about it, let us feel it by examples
Example 01
job1 and job2 are all created from GlobalScope, they are independent coroutines
suspend fun main() {
val dispatcher1 = CoroutineDispatchers.io()
val dispatcher2 = CoroutineDispatchers.data()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
printWithThreadInfo(throwable.message + " " + "DefaultUncaughtExceptionHandler")
}
val errorHandler = CoroutineExceptionHandler { context, throwable ->
printWithThreadInfo(throwable.message + " " + context[CoroutineName])
}
val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("1")) {
val job2 = GlobalScope.launch(dispatcher2 + errorHandler + CoroutineName("2")) {
delay(100)
printWithThreadInfo("1")
throw RuntimeException("crash")
}
delay(200)
printWithThreadInfo("2")
}
delay(300)
printWithThreadInfo("3")
delay(999 * 1000L)
}
25 pool-2-thread-1 "1"
25 pool-2-thread-1 "crash CoroutineName(2)"
24 pool-3-thread-1 "2"
26 kotlinx.coroutines.DefaultExecutor "3"
conclusion
- exception in job2 was caught by error handler in job2
- exception in job2 only interrupt job2, not interrupt job1
Example 02
remove error handler for job2 this time
suspend fun main() {
val dispatcher1 = CoroutineDispatchers.io()
val dispatcher2 = CoroutineDispatchers.data()
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
printWithThreadInfo(throwable.message + " " + "DefaultUncaughtExceptionHandler")
}
val errorHandler = CoroutineExceptionHandler { context, throwable ->
printWithThreadInfo(throwable.message + " " + context[CoroutineName])
}
val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("1")) {
val job2 = GlobalScope.launch(dispatcher2 + CoroutineName("2")) {
delay(100)
printWithThreadInfo("1")
throw RuntimeException("crash")
}
delay(200)
printWithThreadInfo("2")
}
delay(300)
printWithThreadInfo("3")
delay(999 * 1000L)
}
25 pool-2-thread-1 "1"
25 pool-2-thread-1 "crash DefaultUncaughtExceptionHandler"
24 pool-3-thread-1 "2"
26 kotlinx.coroutines.DefaultExecutor "3"
conclusion
- if error handler is not set, exception will be handled by thread’s DefaultUncaughtExceptionHandler
- job1 still won’t be interrupted by job2’s exception
UncaughtExceptionHandler
we just show one kind of UncaughtExceptionHandler above
actually, there are three kinds of UncaughtExceptionHandler, in order of priority, they are
- thread’s uncaughtExceptionHandler, just available for current thread
- global thread-default UncaughtExceptionHandler, available for all threads, if thread doesn’t have a self one
- global coroutine UncaughtExceptionHandler, available for all coroutines, do nothing, just print error stacks
Example 03
cancel job1 this time, watch whether job2 will be cancelled next
suspend fun main() {
val dispatcher1 = CoroutineDispatchers.io()
val dispatcher2 = CoroutineDispatchers.data()
val errorHandler = CoroutineExceptionHandler { context, throwable ->
printWithThreadInfo(throwable.message + " " + context[CoroutineName])
}
val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("1")) {
val job2 = GlobalScope.launch(dispatcher2 + errorHandler + CoroutineName("2")) {
delay(300)
printWithThreadInfo("1")
throw RuntimeException("crash")
}
delay(300)
printWithThreadInfo("2")
}
delay(100)
job1.cancel()
printWithThreadInfo("3")
delay(999 * 1000L)
}
25 kotlinx.coroutines.DefaultExecutor "3"
24 pool-2-thread-1 "1"
24 pool-2-thread-1 "crash CoroutineName(2)"
conclusion
- job2 won’t be cancelled, event if job1 is cancelled
- job2 was created inside job1’s block, but they are in fact independent coroutines
Example 04
let job2 become job1’s child coroutine, then cancel job1, test what will happen
suspend fun main() {
val dispatcher1 = CoroutineDispatchers.io()
val dispatcher2 = CoroutineDispatchers.data()
val errorHandler = CoroutineExceptionHandler { context, throwable ->
printWithThreadInfo(throwable.message + " " + context[CoroutineName])
}
val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("1")) {
val job2 = launch(dispatcher2 + errorHandler + CoroutineName("2")) {
printWithThreadInfo("11")
delay(400)
printWithThreadInfo("12")
throw RuntimeException("crash")
}
delay(200)
printWithThreadInfo("2")
}
delay(100)
job1.cancel()
printWithThreadInfo("3")
delay(999 * 1000L)
}
26 pool-2-thread-1 "11"
25 kotlinx.coroutines.DefaultExecutor "3"
conclusion
- job2 started, but was cancelled during suspend period
- job2 was launched by job1’s CoroutineScope, so it’s job1’s child coroutine
- child coroutine will be cancelled, when parent job is cancelled
Example 05
let child job crash this time
suspend fun main() {
val dispatcher1 = CoroutineDispatchers.io()
val dispatcher2 = CoroutineDispatchers.data()
val errorHandler = CoroutineExceptionHandler { context, throwable ->
printWithThreadInfo(throwable.message + " " + context[CoroutineName])
}
val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("1")) {
val job2 = launch(dispatcher2 + errorHandler + CoroutineName("2")) {
printWithThreadInfo("11")
delay(100)
printWithThreadInfo("12")
throw RuntimeException("crash")
}
delay(200)
printWithThreadInfo("2")
}
delay(400)
printWithThreadInfo("3")
delay(999 * 1000L)
}
24 pool-2-thread-1 "11"
24 pool-2-thread-1 "12"
23 pool-3-thread-1 "crash CoroutineName(1)"
26 kotlinx.coroutines.DefaultExecutor "3"
conclusion
- job2 crashed, job1 is also crashed
- exception was handled by job1
- child coroutine’s crash will lead to parent coroutine’s crash
- child coroutine’s will handled by parent coroutine
Example 06
let child coroutine work longer, watch whether parent coroutine complete before child
suspend fun main() {
val dispatcher1 = CoroutineDispatchers.io()
val dispatcher2 = CoroutineDispatchers.data()
val errorHandler = CoroutineExceptionHandler { context, throwable ->
printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
}
lateinit var job1: Job
job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
val job2 = launch(dispatcher2 + errorHandler + CoroutineName("Job2")) {
delay(100)
printWithThreadInfo("1")
println("Job1.isCompleted = ${job1.isCompleted}")
throw RuntimeException("crash")
}
printWithThreadInfo("2")
}
delay(400)
printWithThreadInfo("3")
println("Job1.isCompleted = ${job1.isCompleted}")
delay(999 * 1000L)
}
24 pool-3-thread-1 "2"
26 pool-2-thread-1 "1"
Job1.isCompleted = false
26 pool-2-thread-1 "Job1-crashed"
25 kotlinx.coroutines.DefaultExecutor "3"
Job1.isCompleted = true
conclusion
- parent coroutine won’t complete, when child coroutine is still running
- if we use GlobalScope launching job2, job1 will complete ahead, as it’s not parent this time
Example 07
start coroutine through async way
@OptIn(InternalCoroutinesApi::class)
suspend fun main() {
val dispatcher1 = CoroutineDispatchers.io()
val dispatcher2 = CoroutineDispatchers.data()
val errorHandler = CoroutineExceptionHandler { context, throwable ->
printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
}
val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
val job2 = async(dispatcher2 + errorHandler + CoroutineName("Job2")) {
delay(100)
printWithThreadInfo("1")
throw RuntimeException("crash")
}
delay(200)
printWithThreadInfo("2")
}
delay(400)
printWithThreadInfo("3")
println("job1.isCompleted=${job1.isCompleted}")
println("job1.isCancelled=${job1.isCancelled}")
println("job1.exception=${job1.getCancellationException().javaClass.name}")
delay(999 * 1000L)
}
16 pool-2-thread-1 "1"
15 pool-3-thread-1 "Job1-crashed"
17 kotlinx.coroutines.DefaultExecutor "3"
job1.isCompleted=true
job1.isCancelled=true
job1.exception=kotlinx.coroutines.JobCancellationException
conclusion
- exception handling method for async is same with launch
- when job2 crashed, job1 was cancelled subsequently
Example 08
test calling async in independent coroutine
suspend fun main() {
val dispatcher1 = CoroutineDispatchers.new()
val dispatcher2 = CoroutineDispatchers.data()
val errorHandler = CoroutineExceptionHandler { context, throwable ->
printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
}
val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
val job2 = GlobalScope.async(dispatcher2 + errorHandler + CoroutineName("Job2")) {
delay(200)
printWithThreadInfo("Job2")
throw RuntimeException("crash")
}
delay(400)
printWithThreadInfo("Job1")
}
delay(600)
printWithThreadInfo("Main")
delay(999 * 1000L)
}
16 pool-2-thread-1 "Job2"
15 pool-1-thread-1 "Job1"
17 kotlinx.coroutines.DefaultExecutor "Main"
conclusion
- everything is ok, event error handle is not called
- as async coroutine’s exception will not be thrown until await is called
- but when async coroutine has a parent, parent will still crashed
Example 09
test how await works after job crashed
suspend fun main() {
val dispatcher1 = CoroutineDispatchers.new()
val dispatcher2 = CoroutineDispatchers.data()
val errorHandler = CoroutineExceptionHandler { context, throwable ->
printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
}
val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
val job2 = GlobalScope.async(dispatcher2 + errorHandler + CoroutineName("Job2")) {
delay(200)
printWithThreadInfo("Job2")
throw RuntimeException("crash")
}
delay(400)
job2.await()
printWithThreadInfo("Job1")
}
delay(600)
printWithThreadInfo("Main")
delay(999 * 1000L)
}
16 pool-2-thread-1 "Job2"
15 pool-1-thread-1 "Job1-crashed"
17 kotlinx.coroutines.DefaultExecutor "Main"
conclusion
- job1 crashed during await period
- as no result can be got, await will obviously cause exception
Example 10
test how join works after job crashed
suspend fun main() {
val dispatcher1 = CoroutineDispatchers.new()
val dispatcher2 = CoroutineDispatchers.data()
val errorHandler = CoroutineExceptionHandler { context, throwable ->
printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
}
lateinit var job2: Job
val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
job2 = launch(dispatcher2 + errorHandler + CoroutineName("Job2")) {
delay(200)
printWithThreadInfo("Job2")
throw RuntimeException("crash")
}
delay(400)
printWithThreadInfo("Job1")
}
delay(600)
job1.join()
job2.join()
printWithThreadInfo("Main")
delay(1 * 1000L)
}
16 pool-2-thread-1 "Job2"
15 pool-1-thread-1 "Job1-crashed"
17 kotlinx.coroutines.DefaultExecutor "Main"
conclusion
- job1 crashed as child coroutine crashed
- join works well after crash
Example 11
let child coroutine handle exception by itself
suspend fun main() {
val dispatcher1 = XDispatchers.new()
val dispatcher2 = XDispatchers.new()
val errorHandler = CoroutineExceptionHandler { context, throwable ->
printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
}
val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
supervisorScope {
val job2 = launch(dispatcher2 + errorHandler + CoroutineName("Job2")) {
delay(100)
printWithThreadInfo("Job2")
throw RuntimeException("crash")
}
}
delay(200)
printWithThreadInfo("Job1")
}
delay(400)
printWithThreadInfo("Main")
delay(10 * 1000L)
}
Job2 <tid=18 XDispatchers-new-2e94db>
Job2-crashed <tid=18 XDispatchers-new-2e94db>
Job1 <tid=20 XDispatchers-new-3223e7>
Main <tid=16 kotlinx.coroutines.DefaultExecutor>
conclusion
- job2 crashed, job1 not
- job2’s exception was handled by itself
- if job1 cancelled, job2 will still be cancelled subsequently
- supervisorScope just prevent exception from spreading upwords
SupervisorScope
let’s find how SupervisorScope implemented
public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
return suspendCoroutineUninterceptedOrReturn { uCont ->
val coroutine = SupervisorCoroutine(uCont.context, uCont)
coroutine.startUndispatchedOrReturn(coroutine, block)
}
}
private class SupervisorCoroutine<in T>(
context: CoroutineContext,
uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
override fun childCancelled(cause: Throwable): Boolean = false
}
a SupervisorCoroutine is created, to handle block with origin context and dispatcher
SupervisorCoroutine refused handling child exception, by override childCancelled returning false
Example 12
another form using supervisor scope
suspend fun main() {
val dispatcher1 = XDispatchers.new()
val dispatcher2 = XDispatchers.new()
val errorHandler = CoroutineExceptionHandler { context, throwable ->
printWithThreadInfo(context[CoroutineName]?.name + "-crashed")
}
val job1 = GlobalScope.launch(dispatcher1 + errorHandler + CoroutineName("Job1")) {
val supervisorScope = CoroutineScope(SupervisorJob(coroutineContext[Job]))
val job2 = supervisorScope.launch(dispatcher2 + errorHandler + CoroutineName("Job2")) {
delay(100)
printWithThreadInfo("Job2")
throw RuntimeException("crash")
}
delay(200)
printWithThreadInfo("Job1")
}
delay(400)
printWithThreadInfo("Main")
delay(10 * 1000L)
}
Job2 <tid=21 XDispatchers-new-24b5e7>
Job2-crashed <tid=21 XDispatchers-new-24b5e7>
Job1 <tid=22 XDispatchers-new-b88926>
Main <tid=19 kotlinx.coroutines.DefaultExecutor>
conclusion
- this way is equivalent to
supervisorScope {}
Conclusions
full process for handling coroutine exception
- if is supervisor scope, handle by self exception handler
- if not supervisor scope, and no parent, handle by self exception handler
- if not supervisor scope, and has parent, handle by parent exception handler
- if parent does not have a exception handler, use thread exception handler
- if thread does not have a exception handler, use DefaultUncaughtExceptionHandler for Thread class
- if DefaultUncaughtExceptionHandler not set, use global coroutine exception handler
- if coroutine works in supervisor scope and crashed, parent will not crash, else parent will crash subsequently
- in any case, if parent job is cancelled, child job will also be cancelled
更多推荐
所有评论(0)