Ali P7 boss teaches you to crack the Kotlin coroutine (6) - coroutine suspension

Cracking Kotlin Coroutine (6) - Coroutine Suspension

insert image description here

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.sleepwas , and in coroutines, the corresponding one is delay. sleepLet the thread go to sleep until a certain signal or condition arrives after the specified time, the thread will try to resume execution, and delaythe coroutine will be suspended. This process will not block the CPU. Nothing is delayed", in this sense, it can delayalso be a good means of letting the coroutine sleep.

delayThe 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.scheduleResumeAfterDelayYou can compare this operation to JavaScript and setTimeoutAndroid handler.postDelay. In essence, it is to set a delay callback, and when the time is up, the resume series of methods will be contcalled to let the coroutine continue to execute.

The most important thing left suspendCancellableCoroutineis , this is our old friend, we used it to realize various conversions from callbacks to coroutines - the original delayis also based on it, if we look at some more source code, you can You will find that there are similar join, and awaitso on.

2. Let’s talk about suspendCancellableCoroutine

Now that everyone is suspendCancellableCoroutinealready , 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 Jobof has been completed, if so, it will directly return and continue to execute the following code without suspending, otherwise it will go to joinSuspendthe branch of this . We see that only a completion callback is registered here, so what exactly does the legendary suspendCancellableCoroutineinternally 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?
    }

suspendCoroutineUninterceptedOrReturnThe source code of this method call is invisible, because it has no source code at all: P Its logic is to help everyone get Continuationthe instance , and that's really the only way. But this is still very abstract, because there is one very suspicious thing: suspendCoroutineUninterceptedOrReturnthe 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 suspendthe function . At that time, I took as awaitan example. This method is roughly equivalent to:

fun await(continuation: Continuation<User>): Any {
    ...
}

suspendOn the one hand, Continuationa , on the other hand, the original return value type Userbecomes the generic argument Continuationof , but the real return value type is actually Any. Of course, because the defined logical return value type Useris non-nullable, the real return value type is also Anyused 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 awaitof , you will also see this getResult()call.

To put it simply, it is not necessary to suspend suspendthe 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 awaitor other suspendfunctions, 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 suspendfunction , which suspendCoroutineUninterceptedOrReturndirectly 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 resumeWithof Result, why is it Objecthere ? Because Resultit 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 helloat , 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 jointhe awaittwo 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 ContinuationImplthat is an implementation Continuationof .

In fact, if you want, you can still find a class ContinuationImplnamed , but its resumeWithis finally called invokeSuspend, and this is invokeSuspendactually our coroutine body, usually a Lambda expression —— We launchstart , and the Lambda expression passed in will actually be compiled into a SuspendLambdasubclass of , which is a subclass ContinuationImplof .

With this class, we also need to prepare a completion to receive the result. This class is implemented like the standard library RunSuspendclass . 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 resultit 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 RunSuspendthe instance resumeWithis ContinuationImplactually resumeWtihcalled at the end of the , so await()once enters the blocking state, ContinuationImplit 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 isContinuation
  • The execution of the coroutine body is a state machine. Every time a suspend function is encountered, it is a state transfer, just like labelthe 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 resumeWithat , 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

img

Chapter 2 Preliminary Explanation of Key Knowledge Points of Kotlin Coroutine

​ ● Coroutine Scheduler

​ ● Coroutine context

​ ● Coroutine startup mode

​ ● Coroutine scope

​ ● suspend function

img

Chapter 3 Exception Handling of Kotlin Coroutines

​ ● Generation process of coroutine exception

​ ● Exception handling for coroutines

img

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

img

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

Guess you like

Origin blog.csdn.net/Android_XG/article/details/130346236