Kotlin Coroutine Cancellation and Exception Handling Exploration Tour (Part 1)
Foreword
Coroutine series articles:
- A short story to explain the relationship between processes, threads, and Kotlin coroutines?
- Young man, do you know what Kotlin coroutines looked like at first?
- To be honest, the suspend/resume of Kotlin coroutines is not so mysterious (story)
- To be honest, the suspend/resume of Kotlin coroutines is not so mysterious (principle)
- It’s time for Kotlin coroutine scheduling and switching threads to unravel the truth
- Kotlin coroutine thread pool exploration tour (with Java thread pool PK)
- Kotlin Coroutine Cancellation and Exception Handling Exploration Tour (Part 1)
- Kotlin Coroutine Cancellation and Exception Handling Exploration Tour (Part 2)
- Come, Kotlin runBlocking/ with me launch/join/async/delay principle & use
- Come on, come with me in the deep water area of Kotlin Channel
- Kotlin coroutine Select: see how I multiplex
- Kotlin Sequence is time to come in handy
- Kotlin Flow backpressure and thread switching are so similar
- Kotlin Flow, where are you going?
- How hot is the Kotlin SharedFlow&StateFlow heat flow?
We know that threads can be terminated, exceptions can be thrown in threads, and similar coroutines will also encounter this situation. This article will start with the analysis of thread termination and exception handling, and gradually introduce the cancellation and exception handling of coroutines.
Through this article, you will learn:
- Termination of thread
- Exception handling of thread
- Job structure of coroutine
1. Thread termination
How to terminate a thread
Terminate in blocking state
Look at a demo first:
class ThreadDemo {
fun testStop() {
//construct thread
var t1 = thread {
println("thread start")
Thread. sleep(2000)
println("thread end")
}
//Interrupt the thread after 1s
Thread. sleep(1000)
t1. interrupt()
}
}
fun main(args : Array) {
var threadDemo = ThreadDemo()
threadDemo. testStop()
}
The result is as follows:
It can be seen that there is no “thread end” Print it out, indicating that the thread was successfully interrupted.
The nature of the thread being interrupted in the above Demo is:
The Thread.sleep(xx) method will detect the interrupt status, and if an interrupt is found, an exception will be thrown.
Terminate in non-blocking state
Reform the demo:
class ThreadDemo {
fun testStop() {
//construct thread
var t1 = thread {
var count = 0
println("thread start")
while (count < 100000000) {
count++
}
println("thread end count: $count")
}
//Wait for the thread to run
Thread. sleep(10)
println("interrupt t1 start")
t1. interrupt()
println("interrupt t1 end")
}
}
The results are as follows:
It can be seen that after the thread is started, the thread is interrupted, and the thread still runs normally until the end, indicating that the thread has not been interrupted at this time.
Essential reason:
The interrupt() method just wakes up the thread and sets the interrupt flag bit.
How to terminate a thread in this scenario? Let’s continue to modify the Demo:
class ThreadDemo {
fun testStop() {
//construct thread
var t1 = thread {
var count = 0
println("thread start")
// Check if interrupted
while (count < 100000000 && !Thread. interrupted()) {
count++
}
println("thread end count: $count")
}
//Wait for the thread to run
Thread. sleep(10)
println("interrupt t1 start")
t1. interrupt()
println("interrupt t1 end")
}
}
Compared with the previous Demo, only the interrupt mark detection is added: Thread.interrupted().
This method returns true to indicate that the thread is interrupted, so we manually stop counting.
The result is as follows:
It can be seen that the thread is terminated successfully up.
To sum up, how to terminate a thread we have a conclusion:
For a more in-depth analysis of the principles and the combination of the two, please move to: Java “gracefully” interrupts threads (practice)
2. Thread exception handling
No matter in Java or Kotlin, exceptions can be caught by try…catch.
Typical as follows:
fun testException() {
try {
1/0
} catch (e : Exception) {
println("e:$e")
}
}
Result:
The exception was caught successfully.
Reform the demo:
fun testException() {
try {
//start the thread
thread {
1/0
}
} catch (e : Exception) {
println("e:$e")
}
}
Let’s guess the result first, can you catch the exception?
Then look at the results:
Unfortunately, unable to capture.
Root cause:
Exception capture is for the stack of the current thread. The above Demo is captured in the main (main) thread, and the exception occurs in the sub-thread.
You may say, simply, I can capture it directly in the sub-thread.
fun testException() {
thread {
try {
1/0
} catch (e : Exception) {
println("e:$e")
}
}
}
There is nothing wrong with doing this, it is reasonable and straightforward.
Consider another scenario: If the main thread wants to get the cause of the sub-thread exception, and then do different processing.
This time it was introduced: UncaughtExceptionHandler.
Continue to transform the Demo:
fun testException3() {
try {
//start the thread
var t1 = thread(false){
1/0
}
t1.name = "myThread"
//set up
t1.setUncaughtExceptionHandler { t, e ->
println("${t.name} exception:$e")
}
t1. start()
} catch (e : Exception) {
println("e:$e")
}
}
In fact, a callback is registered, and the uncaughtException(xx) method will be called when an exception occurs in the thread.
The result is as follows:
It means that the exception was successfully caught.
3. Job structure of coroutine
Job Basics
Creation of Job
Before analyzing the cancellation and exception of the coroutine, we must first figure out the structure of the parent-child coroutine.
class JobDemo {
fun testJob() {
//parent job
var rootJob: Job? = null
runBlocking {
//Promoter Job
var job1 = launch {
println("job1")
}
//Promoter Job
var job2 = launch {
println("job2")
}
rootJob = coroutineContext[Job]
job1. join()
job2. join()
}
}
}
As above, a coroutine is started by runBlocking. At this time, it acts as a parent coroutine, and two coroutines are started in turn as child coroutines in the parent coroutine.
The launch() function is an extension function of CoroutineScope, its function is to start a coroutine:
#Builders.common.kt
fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart. DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
//Construct a new context
val newContext = newCoroutineContext(context)
// coroutine
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
// open
coroutine. start(start, coroutine, block)
// return coroutine
return coroutine
}
Take returning StandaloneCoroutine as an example, it inherits from AbstractCoroutine, and then inherits from JobSupport, and JobSupport implements the Job interface, and the specific implementation class is JobSupport.
We know that coroutines are relatively abstract things, and Job, as a concrete expression of coroutines, represents the jobs of coroutines.
Through Job, we can control and monitor some states of the coroutine, Such as:
//properties
job.isActive //Whether the coroutine is active
job.isCancelled //Whether the coroutine is canceled
job.isCompleted//Whether the coroutine is executed
...
//function
job.join()//wait for the coroutine to complete
job.cancel()//Cancel the coroutine
job.invokeOnCompletion()//register coroutine completion callback
...
Job storage
In the Demo, two child coroutines are started through launch(), exposing two child jobs, and where are their parent jobs?
Find the answer from runBlocking():
#Builers.kt
fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
//...
//Create BlockingCoroutine, which is also a Job
val coroutine = BlockingCoroutine(newContext, currentThread, eventLoop)
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
return coroutine. joinBlocking()
}
BlockingCoroutine inherits from AbstractCoroutine, and there is a member variable in AbstractCoroutine:
#AbstractCoroutine.kt
//this refers to AbstractCoroutine itself, which is BlockingCoroutine
public final override val context: CoroutineContext = parentContext + this
Not only BlockingCoroutine, StandaloneCoroutine also inherits from AbstractCoroutine, so we can see:
The job instance index is stored in the corresponding Context (context), and the specific Job object can be indexed through context[Job].
Parent-child Job Association
Preliminary establishment of binding relationship
We usually refer to coroutines as structured concurrency. Its status, such as exceptions, can be passed between coroutines. How do you understand the concept of structure? The key point is to understand how parent-child coroutines and parallel child coroutines are related.
Still the demo above, with a little modification:
fun testJob2() {
runBlocking {//parent Job==rootJob
//Promoter Job
var job1 = launch {
println("job1")
}
}
}
Start the analysis from the creation of job1, first look at the implementation of AbstractCoroutine:
#AbstractCoroutine.kt
abstract class AbstractCoroutine(
parentContext: CoroutineContext, // the context of the parent coroutine
initParentJob: Boolean,//Whether to associate parent-child Job, default true
active: Boolean //default true
) : JobSupport(active), Job, Continuation, CoroutineScope {
init {
//Associate parent and child Job
//parentContext[Job] is to take out the parent Job from the parent Context
if (initParentJob) initParentJob(parentContext[Job])
}
}
#JobSupport.kt
protected fun initParentJob(parent: Job?) {
if (parent == null) {
//There is no parent Job, the root Job has no parent Job
parentHandle = NonDisposableHandle
return
}
parent.start() // make sure the parent is started
//Bind parent-child Job ①
val handle = parent. attachChild(this)
//Return to the parent Handle, pointing to the linked list ②
parentHandle = handle
//...
}
Divided into two points ① and ②, first look at ①:
#JobSupport.kt
//ChildJob is an interface, and the functions in the interface are used to cancel the child Job for the parent Job
//JobSupport implements the ChildJob interface
public final override fun attachChild(child: ChildJob): ChildHandle {
//ChildHandleNode(child) constructs a ChildHandleNode object
return invokeOnCompletion(onCancelling = true, handler = ChildHandleNode(child).asHandler) as ChildHandle
}
#JobSupport.kt
public final override fun invokeOnCompletion(
onCancelling: Boolean,
invokeImmediately: Boolean,
handler: CompletionHandler
): DisposableHandle {
//create
val node: JobNode = makeNode(handler, onCancelling)
loopOnState { state ->
when (state) {
//According to the state, combine into a linked list of ChildHandleNode
//More cumbersome, ignore
//Return to the head of the linked list
}
}
}
The ultimate goal is to return ChildHandleNode, which may be a linked list.
Look at ② again, record the returned result in the parentHandle member variable of the sub-Job.
To summarize:
- The parent Job constructs a ChildHandleNode node and puts it into a linked list. Each node stores the child Job and the parent Job itself, and the linked list can be exchanged with the state in the parent Job.
- The member variable parentHandle of the sub-Job points to the linked list.
From step 1.2, we can see that the child Job can access the parent Job through the parentHandle, and the parent JobThrough the state, you can find out the child Job associated with it, so that the parent-child Job establishes a connection.
Job chain construction
The above analyzes how the parent-child Jobs are connected, and then focuses on analyzing how the child Jobs are related.
Focus on the structure of ChildHandleNode:
#JobSupport.kt
//Mainly have 2 member variables
//childJob: ChildJob indicates the child Job pointed to by the current node
//parent: Job indicates the parent Job pointed to by the current node
internal class ChildHandleNode(
@JvmField val childJob: ChildJob
) : JobCancellingNode(), ChildHandle {
override val parent: Job get() = job
//The parent job cancels all its child jobs
override fun invoke(cause: Throwable?) = childJob. parentCancelled(job)
//The child job is passed upwards, and the parent job is canceled
override fun childCancelled(cause: Throwable): Boolean = job.childCancelled(cause)
}
It can be seen that the invoke() and childCancelled() functions in ChildHandleNode ultimately rely on Job to realize their functions.
Through searching, it is easy to find that the parentCancelled()/childCancelled() functions are both implemented in JobSupport.
ChildHandleNode finally inherits from LockFreeLinkedListNode, this class is a thread-safe doubly linked list, we can easily imagine that the core of the doubly linked list is to rely on the front and rear pointers.
#LockFreeLinkedList.kt
public actual open class LockFreeLinkedListNode {
// rear drive pointer
private val _next = atomic(this) // Node | Removed | OpDescriptor
//predecessor pointer
private val _prev = atomic(this) // Node to the left (cannot be marked as removed)
private val _removedRef = atomic(null) // lazy cach
}
So the ChildHandleNode linked list is as follows:
In this way, the jobs are connected through the front/back pointers.
Combined with the actual Demo to illustrate the construction process of the Job chain.
fun testJob2() {
runBlocking {//parent Job==rootJob
//Promoter Job
var job1 = launch {
println("job1")
}
//Promoter Job
var job2 = launch {
println("job2")
}
cancel("")
}
}
Step 1
runBlocking creates a coroutine and constructs a Job, which is a BlockingCoroutine. When creating a Job, it will try to bind the parent Job, and at this time it acts as the root Job, there is no parent Job, so parentHandle = NonDisposableHandle.
At this time, it has not created a child job, so there is no child job in the state.
Step 2
Create the first Job: Job1.
The Job constructed at this time is a StandaloneCoroutine. When the Job is created, it will try to bind the parent Job, and the parent Job will be taken out from the parent Context, which is the BlockingCoroutine. After finding it, the association binding will start.
So, the current structure becomes:
The state of the parent Job (pointing to the head of the linked list) is now a linked list. The node in the linked list is ChildHandleNode, and ChildHandleNode stores the parent Job and the child Job.
Step 3
Create the second Job: Job2.
same ,The constructed Job is StandaloneCoroutine, bound to the parent Job, and the final structure becomes:
In summary:
- Attempt to associate a parent Job when creating a Job.
- If the parent Job exists, construct a ChildHandleNode, which stores the parent Job and the child Job, and stores the ChildHandleNode in the State of the parent Job, and the parentHandle of the child Job points to the ChildHandleNode.
- Create the job again and continue to try to associate the parent job, because the parent job has already associated a sub-job, so you need to hang the new sub-job behind the previous sub-job, thus forming a sub-job linked list.
Simple Job Diagram:
As shown in the figure, it is similar to a tree structure.
When the Job chain is established, the transfer of state is simple.
- The parent Job can find each child Job through the linked list.
- The child Job finds the parent Job through the parentHandle.
- The sub-jobs are indexed through the linked list.
Due to space reasons, the cancellation and exception of the coroutine will be analyzed in the next article, so stay tuned.
This article is based on Kotlin 1.5.3, please click here for the complete Demo
If you like it, please like, follow, bookmark, your drumEncouragement is my driving force
Continuously updating, step by step with me system, in-depth study of Android/Kotlin
1. The past and present of Android’s various Contexts
2. Android DecorView must be known
3. Window/WindowManager must-know things
4. View Measure/Layout/Draw is true Understood
5. A full set of Android event distribution services
6. Thoroughly clarify Android invalidate/postInvalidate/requestLayout
7. How to determine the size of Android Window/the reason for multiple executions of onMeasure()
8. Android event-driven Handler-Message-Looper analysis
9. Android keyboard can be done with one trick
10. Android coordinates are completely clear
11. Background of Android Activity/Window/View
/> 12. Android Activity creation to View display
13. Android IPC series
14. Android storage series
15. Java concurrent series no longer doubts
16. Java thread Pool series
17. Android Jetpack front-end basic series
18. Android Jetpack easy-to-learn and easy-to-understand series
19. Kotlin easy entry series
20. Comprehensive interpretation of Kotlin coroutine series