Implementation principle of Kotlin coroutine
Today we are going to talk about Kotlin
coroutines Coroutine
.
If you have been exposed to coroutines, I believe you have the following questions:
- What exactly is a coroutine?
- What is the role of the coroutine
suspend
, and what is the working principle? - What is the relationship between some key names (such as:
Job
,Coroutine
,Dispatcher
,CoroutineContext
and ) in the coroutine?CoroutineScope
- What is the so-called non-blocking suspension and recovery of coroutines?
- What is the internal implementation principle of the coroutine?
- …
The next few articles try to analyze these questions, and everyone is welcome to join in the discussion.
hang up
Coroutines use non-blocking suspension to ensure that coroutines run. So what is non-blocking suspend? Let's talk about what kind of operation it is to hang up.
The keyword was mentioned in the previous article suspend
. One of its functions is that when the code is called, a Continuation
type parameter will be added to the method to ensure the up and down transfer in the coroutine Continuaton
.
Another key role of it is to identify the suspended coroutine.
Whenever a modified method is encountered when the coroutine is running suspend
, the current coroutine may be suspended.
Attention is possible.
You can write a method at will, and the method can also be suspend
modified, but this method will not be suspended when called in a coroutine. For example
private suspend fun a() {
println("aa")
}
lifecycleScope.launch {
a()
}
Because this method does not return COROUTINE_SUSPENDED
a type.
The flag that the coroutine is suspended is the return flag in the corresponding state COROUTINE_SUSPENDED
.
Going a little deeper involves state machines . Inside the coroutine, a state machine is used to manage each suspension point of the coroutine.
The text is a bit abstract, let's look at the code specifically. Let's take the above a
method as an example to illustrate.
First Android Studio
open this code Kotlin Bytecode
. can be Tools -> Kotlin -> Show Kotlin Bytecode
opened in .
Then click one of Decompile
the options to generate the corresponding decompiled java
code. The final code is as follows:
BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
private CoroutineScope p$;
Object L$0;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
// 挂起标识
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
CoroutineScope $this$launch;
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
$this$launch = this.p$;
MainActivity var10000 = MainActivity.this;
// 保存现场
this.L$0 = $this$launch;
// 设置挂起后恢复时,进入的状态
this.label = 1;
// 判断是否挂起
if (var10000.a(this) == var3) {
// 挂起,跳出该方法
return var3;
}
// 不需要挂起,协程继续执行其他逻辑
break;
case 1:
// 恢复现场
$this$launch = (CoroutineScope)this.L$0;
// 是否需要抛出异常
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return Unit.INSTANCE;
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkParameterIsNotNull(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
var3.p$ = (CoroutineScope)value;
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);
The above code is the state machine of the coroutine, which is used to label
represent different states, so as to execute different case
logic codes correspondingly.
As mentioned in the previous article, when the coroutine is started, the resumeWith
method will be called manually once, and its corresponding internal logic is to execute the above invokeSuspend
method.
So when the coroutine is run for the first time, label
the value is 0
, enters case 0:
the statement. At this time, the record site prepares for the state that may be suspended, and sets the next state that may be executed.
If a
the method returns a value var3
, this var3
corresponds to it COROUTINE_SUSPENDED
. So the internal statement will be executed only when a
the method returns , and the method will be jumped out, and the coroutine will be suspended at this time. The current thread can also execute other logic, and will not be blocked by the suspension of the coroutine.COROUTINE_SUSPENDED
if
Therefore, at the code level, the suspension of the coroutine is to jump out of the method body of the coroutine execution, or jump out of the corresponding state under the current state machine of the coroutine, and then wait for the next state to come and execute it.
So why do we say that the method we wrote a
will not be suspended?
@Nullable
final Object a(@NotNull Continuation $completion) {
return Unit.INSTANCE;
}
It turns out that its return value is not COROUTINE_SUSPENDED
.
Since it will not be suspended, under what circumstances will the method be suspended?
It's simple, if we a
put delay
a method in the method, it will be suspended.
@Nullable
final Object a(@NotNull Continuation $completion) {
Object var10000 = DelayKt.delay(1000L, $completion);
return var10000 == IntrinsicsKt.getCOROUTINE_SUSPENDED() ? var10000 : Unit.INSTANCE;
}
It's the method that actually triggers the hang delay
, because delay
the method creates itself Continuation
while internally calling getResult
the method.
internal fun getResult(): Any? {
installParentCancellationHandler()
if (trySuspend()) return COROUTINE_SUSPENDED
// otherwise, onCompletionInternal was already invoked & invoked tryResume, and the result is in the state
val state = this.state
if (state is CompletedExceptionally) throw recoverStackTrace(state.cause, this)
return getSuccessfulResult(state)
}
In getResult
the method, it will be trySuspend
judged to suspend the current coroutine. Suspending the coroutine of the parent class is triggered by suspending its own coroutine.
If it's just for testing, you can let a
the method return directlyCOROUTINE_SUSPENDED
private suspend fun a(): Any {
return kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
}
Of course, you must not write like this online, because once you write the coroutine like this, it will always be suspended, because you have no ability to resume it.
recover
Now let's talk about the recovery of coroutines.
The recovery essence of the coroutine is triggered by the method Continuation
.resumeWith
Let's look at an example that can be suspended, and use it to analyze the entire process of coroutine suspension and recovery.
println("main start")
lifecycleScope.launch {
println("async start")
val b = async {
delay(2000)
"async"
}
b.await()
println("async end")
}
Handler().postDelayed({
println("main end")
}, 1000)
Kotlin
The code is very simple. The current coroutine is running in the main thread, and a async
method is executed internally await
to trigger the suspension of the coroutine.
Let's look at its corresponding decompiled java
code
// 1
String var2 = "main start";
System.out.println(var2);
BuildersKt.launch$default((CoroutineScope)LifecycleOwnerKt.getLifecycleScope(this), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
private CoroutineScope p$;
Object L$0;
Object L$1;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
CoroutineScope $this$launch;
Deferred b;
switch(this.label) {
case 0:
// 2
ResultKt.throwOnFailure($result);
$this$launch = this.p$;
String var6 = "async start";
System.out.println(var6);
b = BuildersKt.async$default($this$launch, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
private CoroutineScope p$;
Object L$0;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
CoroutineScope $this$async;
switch(this.label) {
case 0:
// 3
ResultKt.throwOnFailure($result);
$this$async = this.p$;
this.L$0 = $this$async;
this.label = 1;
if (DelayKt.delay(2000L, this) == var3) {
return var3;
}
break;
case 1:
// 5、6
$this$async = (CoroutineScope)this.L$0;
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
return "async";
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkParameterIsNotNull(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
var3.p$ = (CoroutineScope)value;
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);
this.L$0 = $this$launch;
this.L$1 = b;
this.label = 1;
if (b.await(this) == var5) {
return var5;
}
break;
case 1:
// 7
b = (Deferred)this.L$1;
$this$launch = (CoroutineScope)this.L$0;
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
// 8
String var4 = "async end";
System.out.println(var4);
return Unit.INSTANCE;
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkParameterIsNotNull(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
var3.p$ = (CoroutineScope)value;
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);
// 4
(new Handler()).postDelayed((Runnable)null.INSTANCE, 1000L);
It's a bit long, but it doesn't matter. We only look at the key points and the content related to its state machine.
- First, it will output
main start
, and thenlaunch
enter the coroutine state machine by creating a coroutine, at this timelabel
,0
executecase: 0
the relevant logic. - After
case: 0
entering , output**async start**
, callasync
and passawait
to suspend the current coroutine, record the data of the current suspension point during the suspension process, andlable
set it to1
. - Enter
async
the created coroutine, at this timeasync
in the coroutinelable
is0
, enter the coroutine that isasync case: 0
executeddealy
and suspended .async
andlabel
set to1
. Woke up after waiting2s
. - At this time, the coroutines are all suspended, that is, jump out of the coroutine
launch
method and performhandler
operations. Since it is shorterpost 1s
than in the coroutine , it will output first , and then go through and enter the recovery coroutine stagedealy
**main end**
1s
async
The coroutine in isdelay
resumed. Note that the object ofdelay
is passed in the methodthis
, so once the internal timing is completed, the method will be called to resume the coroutine in, that is, call the method.async
Continuation
delay
2s
Continuation
resumeWith
async
invokeSuspend
async label
Since it has been set to before being suspended1
, it enterscase: 1
, resumes the previously suspended site, checks for exceptions, and finally returnsasync
.- At this time
await
, the suspension point is restored. Note that it is also passed inthis
, and the corresponding one islaunch
in the middleContinuation
, so the method will be called backresumeWith
, and finally calledinvokeSuspend
, that is, entercase 1:
the recovery scene and end the state machine. - Finally, continue to output
async end
, and the coroutine operation ends.
We can execute the above code to verify that the output is correct
main start
async start
main end
async end
Let's sum up, the coroutine is used to suspend
identify the suspension point, but the real suspension point needs to be COROUTINE_SUSPENDED
judged by whether it returns, and the code reflects the suspension and recovery of the coroutine through the state machine. When you need to suspend, first keep the scene and set the next state point, and then suspend the coroutine by exiting the method. The current thread is not blocked during the suspension. The corresponding recovery passes resumeWith
to enter the next state of the state machine, and at the same time, the previously suspended scene will be resumed when entering the next state.
This article mainly introduces the principle of suspending and resuming the coroutine, and also analyzes the execution process related to the state machine of the coroutine. I hope it will be helpful to those who are learning coroutines, and please look forward to the follow-up coroutine analysis.
If you need more Kotlin learning materials, you can scan the QR code to get them for free!
" The most detailed Android version of kotlin coroutine entry advanced combat in history "
Chapter 1 Introduction to the Basics of Kotlin Coroutines
● What is a coroutine
● What is Job, Deferred, and coroutine scope
● Basic usage of Kotlin coroutines
Chapter 2 Preliminary Explanation of Key Knowledge Points of Kotlin Coroutine
● Coroutine Scheduler
● Coroutine context
● Coroutine startup mode
● Coroutine scope
● suspend function
Chapter 3 Exception Handling of Kotlin Coroutines
● Generation process of coroutine exception
● Exception handling for coroutines