Cracking Kotlin Coroutine (6) - Coroutine Suspension
Keywords: Kotlin coroutine coroutine suspend task suspend non-blocking
The suspension of coroutines was a very mysterious thing at first, because we always think in terms of threads, so we can only think of blocking. What the hell is going on with non-blocking hangs? You might laugh when you say it~~ (Crying? . . Sorry, I really can’t write this article more easily, everyone must practice it by yourself!)
1. Look at the delay first
When we first learned about threads, the most common way to simulate various delays Thread.sleep
was , and in coroutines, the corresponding one is delay
. sleep
Let the thread go to sleep until a certain signal or condition arrives after the specified time, the thread will try to resume execution, and delay
the coroutine will be suspended. This process will not block the CPU. Nothing is delayed", in this sense, it can delay
also be a good means of letting the coroutine sleep.
delay
The source code is actually very simple:
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
cont.context.delay.scheduleResumeAfterDelay
You can compare this operation to JavaScript and setTimeout
Android handler.postDelay
. In essence, it is to set a delay callback, and when the time is up, the resume series of methods will be cont
called to let the coroutine continue to execute.
The most important thing left suspendCancellableCoroutine
is , this is our old friend, we used it to realize various conversions from callbacks to coroutines - the original delay
is also based on it, if we look at some more source code, you can You will find that there are similar join
, and await
so on.
2. Let’s talk about suspendCancellableCoroutine
Now that everyone is suspendCancellableCoroutine
already , let's call an old friend directly to you:
private suspend fun joinSuspend() = suspendCancellableCoroutine<Unit> { cont ->
cont.disposeOnCancellation(invokeOnCompletion(handler = ResumeOnCompletion(this, cont).asHandler))
}
Job.join()
This method will first check whether the state Job
of has been completed, if so, it will directly return and continue to execute the following code without suspending, otherwise it will go to joinSuspend
the branch of this . We see that only a completion callback is registered here, so what exactly does the legendary suspendCancellableCoroutine
internally do?
public suspend inline fun <T> suspendCancellableCoroutine(
crossinline block: (CancellableContinuation<T>) -> Unit
): T =
suspendCoroutineUninterceptedOrReturn { uCont ->
val cancellable = CancellableContinuationImpl(uCont.intercepted(), resumeMode = MODE_CANCELLABLE)
block(cancellable)
cancellable.getResult() // 这里的类型是 Any?
}
suspendCoroutineUninterceptedOrReturn
The source code of this method call is invisible, because it has no source code at all: P Its logic is to help everyone get Continuation
the instance , and that's really the only way. But this is still very abstract, because there is one very suspicious thing: suspendCoroutineUninterceptedOrReturn
the return value type of the lambda is T
, and the return value type of the passed lambda is Any?
, that is, cancellable.getResult()
the is Any?
, why is this?
I remember that at the beginning of the coroutine series of articles, I mentioned the signature of suspend
the function . At that time, I took as await
an example. This method is roughly equivalent to:
fun await(continuation: Continuation<User>): Any {
...
}
suspend
On the one hand, Continuation
a , on the other hand, the original return value type User
becomes the generic argument Continuation
of , but the real return value type is actually Any
. Of course, because the defined logical return value type User
is non-nullable, the real return value type is also Any
used to indicate that if the generic argument is a nullable type, then the real return value type Any?
is , which is exactly Corresponds to this cancellable.getResult()
returned by the aforementioned .Any?
If you check the source code
await
of , you will also see thisgetResult()
call.
To put it simply, it is not necessary to suspend suspend
the function , it can be suspended when needed, that is, when the coroutine to be waited for has not finished executing, wait for the coroutine to be executed before continuing to execute; and if at the beginning Orjoin
await
or other suspend
functions, if the target coroutine has been completed, then there is no need to wait, just take the result and leave. Then the magic logic lies cancellable.getResult()
in what is returned, let's see:
internal fun getResult(): Any? {
...
if (trySuspend()) return COROUTINE_SUSPENDED // ① 触发挂起逻辑
...
if (state is CompletedExceptionally) // ② 异常立即抛出
throw recoverStackTrace(state.cause, this)
return getSuccessfulResult(state) // ③ 正常结果立即返回
}
① of this code is the suspend logic, which means that the target coroutine has not finished executing at this time and needs to wait for the result. ②③ is the two cases where the coroutine has been executed and the abnormal and normal results can be obtained directly. ②③It is easy to understand, the key is ①, it is going to hang, what is it returning?
public val COROUTINE_SUSPENDED: Any get() = CoroutineSingletons.COROUTINE_SUSPENDED
internal enum class CoroutineSingletons { COROUTINE_SUSPENDED, UNDECIDED, RESUMED }
This is the implementation of 1.3, the implementation before 1.3 is more interesting, it is a whiteboard Any
. In fact, it doesn't matter what it is. The key is that this thing is a singleton. Whenever the coroutine sees it, it knows that it should hang up.
3. Dive into pending operations
Now that we talk about hanging, you may feel that you still don’t know much about it, or you still don’t know how to do it, what should you do? To be honest, what is the operation of suspending has not been shown to everyone. It is not that we are too stingy, but it will be more scary if it is taken out too early. .
suspend fun hello() = suspendCoroutineUninterceptedOrReturn<Int>{
continuation ->
log(1)
thread {
Thread.sleep(1000)
log(2)
continuation.resume(1024)
}
log(3)
COROUTINE_SUSPENDED
}
I wrote such a suspend
function , which suspendCoroutineUninterceptedOrReturn
directly returns the legendary whiteboard COROUTINE_SUSPENDED
. Normally, we should call this method in a coroutine, right? But I don’t, I write a piece of Java code to call this method, the result what will happen?
public class CallCoroutine {
public static void main(String... args) {
Object value = SuspendTestKt.hello(new Continuation<Integer>() {
@NotNull
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) { // ①
if(o instanceof Integer){
handleResult(o);
} else {
Throwable throwable = (Throwable) o;
throwable.printStackTrace();
}
}
});
if(value == IntrinsicsKt.getCOROUTINE_SUSPENDED()){ // ②
LogKt.log("Suspended.");
} else {
handleResult(value);
}
}
public static void handleResult(Object o){
LogKt.log("The result is " + o);
}
}
This code looks strange, and there are two things that may be confusing:
①, we see the parameter type resumeWith
of Result
, why is it Object
here ? Because Result
it is an inline class, it will be replaced with its only member at compile time, so it is replaced by Object
(in Kotlin Any?
)
② IntrinsicsKt.getCOROUTINE_SUSPENDED()
is Kotlin'sCOROUTINE_SUSPENDED
The rest is actually not difficult to understand, and the running result is naturally as follows:
07:52:55:288 [main] 1
07:52:55:293 [main] 3
07:52:55:296 [main] Suspended.
07:52:56:298 [Thread-0] 2
07:52:56:306 [Thread-0] The result is 1024
In fact, the calling method of this Java code is very close to the following call in Kotlin:
suspend fun main() {
log(hello())
}
It's just that it's still not easy for us to get the real return value hello
at , and the other return results are exactly the same.
12:44:08:290 [main] 1
12:44:08:292 [main] 3
12:44:09:296 [Thread-0] 2
12:44:09:296 [Thread-0] 1024
It is very likely that you will feel dizzy when you see this. It doesn’t matter. I have now begun to try to reveal the logic behind some coroutine suspension. Compared with simple use, understanding and acceptance of concepts requires a small process.
4. In-depth understanding of the state transition of coroutines
We have already made some revelations about the principle of coroutines. Obviously, the Java code makes it easier for everyone to understand, so let's look at a more complicated example:
suspend fun returnSuspended() = suspendCoroutineUninterceptedOrReturn<String>{
continuation ->
thread {
Thread.sleep(1000)
continuation.resume("Return suspended.")
}
COROUTINE_SUSPENDED
}
suspend fun returnImmediately() = suspendCoroutineUninterceptedOrReturn<String>{
log(1)
"Return immediately."
}
We first define two suspending functions, the first will actually suspend, and the second will directly return the result, which is similar to join
the await
two paths we discussed earlier or . Let's give an example of calling them again in Kotlin:
suspend fun main() {
log(1)
log(returnSuspended())
log(2)
delay(1000)
log(3)
log(returnImmediately())
log(4)
}
The result of the operation is as follows:
08:09:37:090 [main] 1
08:09:38:096 [Thread-0] Return suspended.
08:09:38:096 [Thread-0] 2
08:09:39:141 [kotlinx.coroutines.DefaultExecutor] 3
08:09:39:141 [kotlinx.coroutines.DefaultExecutor] Return immediately.
08:09:39:141 [kotlinx.coroutines.DefaultExecutor] 4
Ok, now we want to reveal the real face of this coroutine code. In order to do this, we use Java to imitate this logic:
Note that the following code cannot be logically rigorous and should not appear in production. It is only for learning and understanding coroutines.
public class ContinuationImpl implements Continuation<Object> {
private int label = 0;
private final Continuation<Unit> completion;
public ContinuationImpl(Continuation<Unit> completion) {
this.completion = completion;
}
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object o) {
try {
Object result = o;
switch (label) {
case 0: {
LogKt.log(1);
result = SuspendFunctionsKt.returnSuspended( this);
label++;
if (isSuspended(result)) return;
}
case 1: {
LogKt.log(result);
LogKt.log(2);
result = DelayKt.delay(1000, this);
label++;
if (isSuspended(result)) return;
}
case 2: {
LogKt.log(3);
result = SuspendFunctionsKt.returnImmediately( this);
label++;
if (isSuspended(result)) return;
}
case 3:{
LogKt.log(result);
LogKt.log(4);
}
}
completion.resumeWith(Unit.INSTANCE);
} catch (Exception e) {
completion.resumeWith(e);
}
}
private boolean isSuspended(Object result) {
return result == IntrinsicsKt.getCOROUTINE_SUSPENDED();
}
}
We define a Java class ContinuationImpl
that is an implementation Continuation
of .
In fact, if you want, you can still find a class
ContinuationImpl
named , but itsresumeWith
is finally calledinvokeSuspend
, and this isinvokeSuspend
actually our coroutine body, usually a Lambda expression —— Welaunch
start , and the Lambda expression passed in will actually be compiled into aSuspendLambda
subclass of , which is a subclassContinuationImpl
of .
With this class, we also need to prepare a completion to receive the result. This class is implemented like the standard library RunSuspend
class . If you have read the previous article, then you should know that the implementation of suspend main is based on this class:
public class RunSuspend implements Continuation<Unit> {
private Object result;
@Override
public CoroutineContext getContext() {
return EmptyCoroutineContext.INSTANCE;
}
@Override
public void resumeWith(@NotNull Object result) {
synchronized (this){
this.result = result;
notifyAll(); // 协程已经结束,通知下面的 wait() 方法停止阻塞
}
}
public void await() throws Throwable {
synchronized (this){
while (true){
Object result = this.result;
if(result == null) wait(); // 调用了 Object.wait(),阻塞当前线程,在 notify 或者 notifyAll 调用时返回
else if(result instanceof Throwable){
throw (Throwable) result;
} else return;
}
}
}
}
The key point of this code is await()
the method , which creates an infinite loop, but don't be afraid, this infinite loop is a paper tiger, if result
it is null
, then the current thread will be blocked immediately until the result appears. The specific usage method is as follows:
...
public static void main(String... args) throws Throwable {
RunSuspend runSuspend = new RunSuspend();
ContinuationImpl table = new ContinuationImpl(runSuspend);
table.resumeWith(Unit.INSTANCE);
runSuspend.await();
}
...
This way of writing is simply the true face of suspend main.
We see that RunSuspend
the instance resumeWith
is ContinuationImpl
actually resumeWtih
called at the end of the , so await()
once enters the blocking state, ContinuationImpl
it will not stop blocking until the overall state of the is completed, and the process will run normally at this time quit.
So the result of running this code is as follows:
08:36:51:305 [main] 1
08:36:52:315 [Thread-0] Return suspended.
08:36:52:315 [Thread-0] 2
08:36:53:362 [kotlinx.coroutines.DefaultExecutor] 3
08:36:53:362 [kotlinx.coroutines.DefaultExecutor] Return immediately.
08:36:53:362 [kotlinx.coroutines.DefaultExecutor] 4
We see that this plain Java code is exactly the same as the previous Kotlin coroutine call. So what is the basis for my Java code? It is the bytecode generated after Kotlin coroutine compilation. Of course, the bytecode is relatively abstract. I wrote it like this to make it easier for everyone to understand how the coroutine is executed. Seeing this, I believe everyone has a better understanding of the essence of the coroutine:
- The suspending function of the coroutine is essentially a callback, and the callback type is
Continuation
- The execution of the coroutine body is a state machine. Every time a suspend function is encountered, it is a state transfer, just like
label
the continuous to realize the state flow
If you can understand these two points clearly, then I believe that when you learn other concepts of coroutines, it will no longer be a problem. If you want to perform thread scheduling, just follow the method of the scheduler we mentioned and perform thread switching resumeWith
at , which is actually very easy to understand. The official coroutine framework is essentially doing such a few things. If you look at the source code, you may be confused for a while, mainly because the framework needs to consider cross-platform implementation in addition to implementing core logic, and also needs to optimize performance. But no matter what, the source code looks like five words: state machine callback.
5. Summary
Different from the past, we start from this article to unreservedly try to reveal the logic behind the coroutine for everyone. It may be difficult to understand for a while, but it doesn’t matter. You can read these contents after using the coroutine for a while. I believe it will It suddenly became clear.
Kotlin coroutine learning materials can be obtained for free by scanning the QR code below!
# "**The most detailed introduction to Android version kotlin coroutine advanced actual 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
Chapter 4 Basic application of kotlin coroutines in Android
● Android uses kotlin coroutines
● Use coroutines in Activity and Framgent
● Use coroutines in ViewModel
● Use coroutines in other environments
Chapter 5 Network request encapsulation of kotlin coroutine
● Common environments for coroutines
● Encapsulation and use of coroutines under network requests
● Higher-order function method
● Multi-state function return value method