Android coroutine

coroutine


Before introducing coroutines, let us first understand a few concepts

1. Let’s talk about some related concepts before coroutines

1. Concurrency and parallelism

In the operating system, we have learned about concurrency and parallelism

Concurrency means that only one instruction is executed at the same time , and other instructions are not executed again. However, because the time slice of the CPU is extremely short, the time interval between multiple instructions switching back and forth is extremely short , as if multiple instructions are executing at the same time . Both single-core CPU and multi-core CPU can perform concurrency

Parallelism is different. At the same time , multiple instructions are executed . This is a no-brainer. It can only be done in a multi-core CPU .

2. Synchronous and asynchronous

Synchronous operations are very common. We generally run a program and can only execute other programs after the program is completed.

As for asynchronous operation, if our program encounters a function that loops 10 times, our program may not directly loop 10 times, but skip this program and execute other programs.

In okhttp3, there are two request methods: synchronous and asynchronous.

Synchronization is as follows

        OkHttpClient okHttpClient = new OkHttpClient();//1.定义一个client
        Request request = new Request.Builder().url("http://www.baidu.com").build();//2.定义一个request
        Call call = okHttpClient.newCall(request);//3.使用client去请求
        try {
    
    
            String result = call.execute().body().string();//4.获得返回结果
            System.out.println(result);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }

Asynchronous is as follows

        OkHttpClient okHttpClient = new OkHttpClient();//1.定义一个client
        Request request = new Request.Builder().url("http://www.baidu.com").build();//2.定义一个request
        Call call = okHttpClient.newCall(request);//3.使用client去请求
        call.enqueue(new Callback() {
    
    //4.回调方法
            @Override
            public void onFailure(Call call, IOException e) {
    
    
 
            }
 
            @Override
            public void onResponse(Call call, Response response) throws IOException {
    
    
                String result = response.body().string();//5.获得网络数据
                System.out.println(result);
            }
        });

We can see that after getting the call synchronously, we get the Response through the call and then convert the Response into the String type .

But when we are asynchronous, when we get the call , we call the enqueue method of the call , and then process it in OnReponse.

By the way, this enqueue is actually used in the handler's post method. After we post, we will call its sendMessageDelay and then call sendMessageAtTime and then call enqueueMessage , so the handler's post method is an asynchronous method.

3. blocking

3.1 Looper’s blocking

3.1.1 loop source code
public static void loop() {
    
    
    final Looper me = myLooper();
    if (me == null) {
    
    
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    if (me.mInLoop) {
    
    
        Slog.w(TAG, "Loop again would have the queued messages be executed"
                + " before this one completed.");
    }

    me.mInLoop = true;

    // Make sure the identity of this thread is that of the local process,
    // and keep track of what that identity token actually is.
    Binder.clearCallingIdentity();
    final long ident = Binder.clearCallingIdentity();

    // Allow overriding a threshold with a system prop. e.g.
    // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'
    final int thresholdOverride =
            SystemProperties.getInt("log.looper."
                    + Process.myUid() + "."
                    + Thread.currentThread().getName()
                    + ".slow", 0);

    me.mSlowDeliveryDetected = false;

    for (;;) {
    
    
        if (!loopOnce(me, ident, thresholdOverride)) {
    
    
            return;
        }
    }
}

We can see that at the end it has a for infinite loop , only if

!loopOnce(me, ident, thresholdOverride)

will exit when

Let’s take a look at the source code of loopOnce

3.1.2loopOnce source code
private static boolean loopOnce(final Looper me,
        final long ident, final int thresholdOverride) {
    
    
    Message msg = me.mQueue.next(); // might block
    if (msg == null) {
    
    
        // No message indicates that the message queue is quitting.
        return false;
    }

    // This must be in a local variable, in case a UI event sets the logger
    final Printer logging = me.mLogging;
    if (logging != null) {
    
    
        logging.println(">>>>> Dispatching to " + msg.target + " "
                + msg.callback + ": " + msg.what);
    }
    // Make sure the observer won't change while processing a transaction.
    final Observer observer = sObserver;

    final long traceTag = me.mTraceTag;
    long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
    long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs;
    if (thresholdOverride > 0) {
    
    
        slowDispatchThresholdMs = thresholdOverride;
        slowDeliveryThresholdMs = thresholdOverride;
    }
    final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0);
    final boolean logSlowDispatch = (slowDispatchThresholdMs > 0);

    final boolean needStartTime = logSlowDelivery || logSlowDispatch;
    final boolean needEndTime = logSlowDispatch;

    if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
    
    
        Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
    }

    final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
    final long dispatchEnd;
    Object token = null;
    if (observer != null) {
    
    
        token = observer.messageDispatchStarting();
    }
    long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid);
    try {
    
    
        msg.target.dispatchMessage(msg);
        if (observer != null) {
    
    
            observer.messageDispatched(token, msg);
        }
        dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
    } catch (Exception exception) {
    
    
        if (observer != null) {
    
    
            observer.dispatchingThrewException(token, msg, exception);
        }
        throw exception;
    } finally {
    
    
        ThreadLocalWorkSource.restore(origWorkSource);
        if (traceTag != 0) {
    
    
            Trace.traceEnd(traceTag);
        }
    }
    if (logSlowDelivery) {
    
    
        if (me.mSlowDeliveryDetected) {
    
    
            if ((dispatchStart - msg.when) <= 10) {
    
    
                Slog.w(TAG, "Drained");
                me.mSlowDeliveryDetected = false;
            }
        } else {
    
    
            if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, "delivery",
                    msg)) {
    
    
                // Once we write a slow delivery log, suppress until the queue drains.
                me.mSlowDeliveryDetected = true;
            }
        }
    }
    if (logSlowDispatch) {
    
    
        showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, "dispatch", msg);
    }

    if (logging != null) {
    
    
        logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
    }

    // Make sure that during the course of dispatching the
    // identity of the thread wasn't corrupted.
    final long newIdent = Binder.clearCallingIdentity();
    if (ident != newIdent) {
    
    
        Log.wtf(TAG, "Thread identity changed from 0x"
                + Long.toHexString(ident) + " to 0x"
                + Long.toHexString(newIdent) + " while dispatching to "
                + msg.target.getClass().getName() + " "
                + msg.callback + " what=" + msg.what);
    }

    msg.recycleUnchecked();

    return true;
}

We don't need to look at so much code, we only need to know when to return true and when to return false

We will find out when

  1. When a message is successfully obtained from the message queue, the distribution and processing of the message will be performed, and at the end, it indicates that return true;a message has been successfully processed and is ready to continue the next cycle.
  2. When the message obtained from the message queue is null, that is, when the message queue is exiting, it will return false;indicate that there are no more messages to process and exit the loop.

At this time we probably understand that when the message queue continues to receive messages, looper.loop will continue to proceed.

The for loop of looper.loop will exit when no message is passed.

3.1.3 Attention

At that time, I equalized blocking and infinite loop, so what I always understood was that when a message is transmitted, the Looper will be blocked, and when no message is transmitted, the Looper will not be blocked. But the common understanding of blocking is that this thread does nothing, just waits. The thread is in a dormant state or cannot be executed for a long time and is stuck there .

So based on this understanding, when no Message is transmitted, Looper is in a blocking state. When a Message comes, Looper is not in a blocking state

Now let's talk about it: Will Looper's endless loop cause ANR ?

3.2Will Looper in an infinite loop cause ANR?

3.2.1What is ANR

In Android, ANR (Application Not Responding) refers to the situation where the application does not respond to user interaction events (such as touching the screen, pressing keys, etc.). When an application does not respond to an event for an extended period of time, the system displays an ANR dialog box, notifying the user that the application has stopped responding and giving the user the option to Force Close or Wait.

Simply put, when a task takes up too many resources, it is easy to cause ANR.

3.2.2 Will Looper’s infinite loop cause ANR?

A common test point in interviews is: Will Looper's infinite loop lead to ANR?

The answer is no, the looper loop will not cause ANR, it may only block the main thread (when no message is passed). It has a message queue. When there is a message in the message queue, it will loop to get it. When there is no message, it will Call epoll.await and then block the main thread. When there is a message again, it will wake up and perform its blocking. ANR is not the same concept. ANR means that the application is unresponsive. If I click an event and it does not respond for a long time, it will cause ANR but blocking. It's because there is no message rather than no response.

What I couldn't figure out at first was that if my looper handles a particularly large task, then why can't I refute whether the looper's infinite loop causes ANR? Then I thought about
processing a particularly large event, which is handled by its dispatchMessage and mine. It doesn't matter if it's an infinite loop. My infinite loop is only responsible for finding messages but not processing them.

This was told to me by a senior student. I originally thought this meant that blocking the main thread would not cause ANR.

Remember the two situations we just mentioned that lead to blocking

1. No news is coming. Waiting for news to come.

2. Handle time-consuming tasks

We are now in the first case. When no message is received, it is actually waiting for new messages to arrive. (Equivalent to a sleep state) It does not occupy too much CPU resources or block other operations. In this case, the main thread can still handle user interface events and other operations. So, when Looperblocking the main thread when no message is received, it will not cause ANR. However, if Handlertime-consuming operations are performed while processing the message, this has the potential to cause ANR.

But this question actually mainly asks about the concepts of Looper's epoll and ANR. I'm thinking a little too much, and I'm a little bit over the top.

3.2.3 Summary

In Looperthe infinite loop, it will continuously take out messages from the message queue and distribute the messages to the corresponding Handlerprocessing. When the message queue is empty, Looperit will wait in a loop for new messages to arrive.

While waiting for messages, Looperthe infinite loop will block the main thread because it will always occupy the execution time slice of the main thread. This means that the main thread cannot continue to perform other tasks or respond to user input events or system events.

ANR errors are triggered only when the main thread occupies the CPU or other system resources for a long time and is unable to respond to user input events or complete key operations for a long time.

The time-consuming operation itself will not cause the main thread to get stuck. The real cause of the main thread getting stuck is that the operations after the time-consuming operation are not distributed within the specified time.

4. hang

Suspending means saving the current state and waiting to resume execution . In Android, suspending means not affecting the work of the main thread. A more appropriate statement can be understood as switching to a specified thread.

4.1 The difference between blocking and suspending

The main difference between them is to free up the CPU

Suspension is cooperative, that is, the thread will be actively released when suspended so that the thread can perform other tasks. At the same time, when the coroutine is suspended, the stack and context of the coroutine will be saved, and execution will be resumed after the suspension is completed. This method can avoid thread switching overhead and improve program performance and response speed.

Blocking is mandatory, that is, the thread will be occupied until the blocking is over, and execution will not continue until the blocking ends. While blocked, the thread cannot perform other tasks, thus wasting CPU resources. At the same time, blocking will also increase the overhead of thread switching and reduce the performance and response speed of the program.

5. Multitasking

Multitasking means that the operating system can handle multiple tasks at the same time

There are two more types of multitasking, one is preemptive multitasking and the other is cooperative multitasking.

  1. Preemptive multitasking means that the operating system itself specifies the CPU occupation time of each task. After this time is exceeded, the operating system itself specifies the CPU occupation time for each task. The current task cannot occupy the CPU and is handed over to the next task.
  2. But cooperative multitasking means that unless your task gives up its possession of the CPU, other tasks cannot use that CPU.

After understanding these concepts, let’s talk about coroutines.

2.Coroutine

1. The role of coroutines

The threads we learned before are very heavyweight, and they need to rely on the scheduling of the operating system to switch between different threads. But coroutines can switch between different coroutines only at the programming language level.

Coroutines allow us to simulate the effects of multi-threaded programming in single-threaded mode

2. Basic usage of coroutines

 implementation'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
    implementation'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'

Import the package first

2.1GlobalScope.launch

Then create a package, note that its package suffix is ​​.kt

fun main(){
    
    
GlobalScope.launch {
    
    
    println("nihao")
    Log.d("TAG","NIHAO")
}
}

Then call main() in MainActivity

We will find that neither nihao nor NIHAO can print it out

This is because: Global.launch creates a top-level coroutine every time , which will end when the application ends. The reason why the log could not be printed just now is because the application program ended before the code in the code block could be executed.

We just need to delay the program for a period of time and end it

fun main(){
    
    
GlobalScope.launch {
    
    
    println("nihao")
    Log.d("TAG","NIHAO")
}
    Thread.sleep(1000)
}

We let the thread where the main is located block for 1000ms and let the main thread sleep . At this time we will find that it is printed

There is a problem with this, that is, if the code cannot be ended within 1000ms, it will be forcibly interrupted.

Such as now

fun main(){
    
    
GlobalScope.launch {
    
    
    println("nihao")
    delay(1500)
    Log.d("TAG","NIHAO")
}
    Thread.sleep(1000)
}

Our current logic is to let the main thread sleep for 1000ms and then execute the code in GlobalScope.launch . We set a delay of 1500ms after println() before executing the content in Log.d().

At this time we will find that only the content in println() will be printed , but the content in **Log.d()** will not be printed.

2.2runBlocking

When we used GlobalScope.launch just now , because it is a top-level coroutine , it will end when the program ends .

So is there a coroutine that will end the program after all the codes in the coroutine have been executed?

This is runBlocking

Its basic format is

fun main1(){
    
    
    runBlocking {
    
    
        println("runBlocking")
    }
}

Can print successfully

runBlocking

runBlocking can ensure that the current thread is blocked until all codes and sub-coroutines in the coroutine scope are executed.

Generally, runBlocking may have performance problems during formal development, so we generally only use it in test environments.

2.3 Create multiple coroutines

Just add launch to the runBlocking just now .

As shown below:

fun main2(){
    
    
    runBlocking{
    
    
        launch {
    
    
            println("launch_0")
        }
        launch {
    
    
            println("launch_1")
        }
    }
}

can be printed out

launch_0
launch_1

You can’t tell anything about this yet, let’s add some code later

fun main2(){
    
    
    runBlocking{
    
    
        launch {
    
    
            println("launch_0")
            delay(1000)
            println("launch_0 finished")
        }
        launch {
    
    
            println("launch_1")
            delay(1000)
            println("launch_1 finished")
        }
    }
}

We add delay(1000) after the two launches are printed , and then print

we will find

launch_0
launch_1
launch_0 finished
launch_1 finished

It does not wait for launch_0 to finish printing and then print launch_0 finshed , but then prints launch_1

Much like concurrent operations in multithreading

Another thing to note: launch here is different from GlobalScope.launch . The former must be called in the scope of the coroutine. Secondly, it will create a sub-coroutine in the scope of the current coroutine (a sub-coroutine is a local coroutine. When the coroutine in the outer scope ends, all sub-coroutines under the scope will also end together)

But GlobalScope.launch always creates a top-level coroutine , which will end when the application ends.

2.4suspend keyword

We have just said that launch must be used within the scope of the coroutine. If we write all launches in runBlocking , then if there are too many launches, then the number of lines occupied by runBlocking will be even more.

So is it possible to write runBlocking first and then call the method?

for example

fun main3(){
    
    
    runBlocking {
    
    
        main4()
        main5()
    }
}
fun main4(){
    
    
    println("没有协程作用域_0")
}
fun main5(){
    
    
    println("没有协程作用域_1")
}

We can successfully print out

没有协程作用域_0
没有协程作用域_1

However, you will find that you cannot call suspending functions like delay() because there is no coroutine scope.

We can solve this problem by suspend keyword

fun main3(){
    
    
    runBlocking {
    
    
        main4()
        main5()
    }
}
suspend fun main4(){
    
    
    delay(1000)
    println("没有协程作用域_0")
}
fun main5(){
    
    
    println("没有协程作用域_1")
}

In this way, suspension functions such as **delay()** are successfully implemented.

Although suspension functions such as delay can be implemented in this way, we cannot call the launch function. It can only be called in the scope of the coroutine.

2.5coroutineScope

The coroutineScope function is a suspend function and can be called from any other suspend function. Its characteristic is that it will inherit the scope of the external coroutine and create a sub-coroutine.

fun main6(){
    
    
    runBlocking {
    
    
        main7()
        main8()
    }
}
suspend fun main7() = coroutineScope{
    
    
   launch {
    
    
       println("coroutineScope_0")
       delay(1000)
   }
}
suspend fun main8() = coroutineScope{
    
    
    launch {
    
    
        println("coroutineScope_1")
        delay(1000)
    }
}

that's it

It can ensure that all code and sub-coroutines within its scope will be suspended until all codes and sub-coroutines are executed.

coroutineScope is not the same as runBlocking

The former will only suspend the current coroutine and will not affect other coroutines or any threads.

However, the latter will suspend external threads. If the function is used in the main thread runBlockingto start a long-running coroutine, the main thread will be blocked, causing the interface to freeze.

2.5.1 Note

I didn't understand this at the time, and then I took a look at whether Looper's infinite loop would cause ANR . My understanding at the time was that when Looper did not receive the message, epoll would issue an await instruction, causing the main thread to be blocked. But blocking does not cause ANR

Then I looked here again: runBlocking will suspend external threads. If the function is used in the main thread runBlockingto start a long-running coroutine, the main thread will be blocked, causing the interface to freeze.

Then I fainted. Isn't it because blocking does not cause ANR? Why did it cause ANR again? Facts have proved that my understanding at the time was wrong. The specific analysis is written above.

Here, the main thread is blocked because of the processing of time-consuming operations. If the event is too long, it will cause ANR.

3.Coroutine scope

Before talking about constructor scope, let's first understand what coroutine scope is .

In GlobalScope.launch , it creates a top-level coroutine , and the code in it will end when the application ends.

In runBlocking we say: runBlocking can ensure that the current thread is blocked until all the codes in the coroutine scope and sub-coroutines are executed.

Then launch , it must rely on the coroutine scope to execute

coroutineScope has similar effects to runBlocking . It can be called in the coroutine scope or suspending function. It's just that coroutineScope will only suspend external coroutines and will not affect others, while runBlocking will directly block the thread.

So what is coroutine scope ?

The explanation given is:

Coroutine Scope refers to the life cycle and scope of the coroutine, which is used to manage the execution scope and life cycle of the coroutine to ensure the safety and correctness of the coroutine.

In coroutines, the CoroutineScope interface is usually used to define the coroutine scope. The CoroutineScope interface provides a set of Coroutine Builder and Coroutine Context for creating and managing the life cycle and scope of coroutines. Coroutine scope can be global or local, and their life cycle can be short-lived or long-term.

Within a coroutine scope, you can create one or more coroutines and organize them into a hierarchy. Each coroutine has its own scope and life cycle, and they can access and share resources and status within the scope. The coroutine scope can also define the cancellation policy and exception handling method of the coroutine to ensure the safety and correctness of the coroutine.

Coroutines must be started in the coroutine scope. Some rules for parent-child coroutines are defined in the coroutine scope. Kotlin coroutines manage and control all coroutines in the domain through the coroutine scope.

Coroutine scopes can be juxtaposed or included to form a tree structure. This is structured concurrency in Kotlin coroutines.

There are three types of scope subdivisions:

Top-level scope : the scope of a coroutine without a parent coroutine (GlobalScope.launch)

Collaborative scope : A new coroutine (i.e. sub-coroutine) is started in the coroutine. At this time, the scope where the sub-coroutine is located defaults to the collaborative scope. All uncaught exceptions thrown by the sub-coroutine will be passed to the parent coroutine for processing. , the parent coroutine will also be canceled at the same time; (coroutineScope)

Master-slave scope : It is consistent with the parent-child relationship of the collaborative scope. The difference is that when an uncaught exception occurs in the child coroutine, it will not be passed up to the parent coroutine.

Rules between parent and child coroutines

If the parent coroutine is canceled or ended, all child coroutines below it will be canceled or ended.

The parent coroutine needs to wait for the child coroutine to finish executing before it finally enters the completion state, regardless of whether the code block of the parent coroutine itself has been executed.

The child coroutine will inherit the elements in the parent coroutine context. If it has members with the same Key, it will overwrite the corresponding Key. The overwriting effect is only valid within its own scope.

4. Scope constructor

When we use top-level coroutines for network loading operations, we may encounter such problems. We want to close an application, but a coroutine is started in the application to perform a network request, and this request has not been completed before the application is closed, then the request will continue to be executed, which may waste system resources, such as network bandwidth and CPU time, and may also cause the request results to be processed incorrectly or cause other problems.

So we need to close the coroutine before exiting the application.

How to cancel the coroutine? Whether it is GlobalScope.launcha function or launcha function, they will return a Job object . You only need to call the **cancle()** method of the Job object to cancel the coroutine.

val job = GlobalScope.launch {
    
     
    //  处理具体逻辑
}
job.cancel()

If we create top-level coroutines every time, then when the Activity is closed, we need to call the cancel() method of all created coroutines one by one. In this case, the code will be difficult to maintain. Therefore, coroutine scope builders such as GlobalScope.launch are not commonly used in actual projects.

4.1 Commonly used writing methods in actual projects

val job = Job()
val scope = CoroutineScope(job)
scope.launch {
    
     
    //  处理具体逻辑
}
job.cancel()

First create a Job object and pass in the CoroutineScope() function. The CoroutineScope() function will return a CoroutineScope object. With this object, you can call the launch function of the CoroutineScope object to create a coroutine.

Therefore, the coroutines created by calling the launch function of CoroutineScope will be associated under the scope of the Job object. In this way, all coroutines in the same scope can be canceled by calling the job.cancel() method once . This is The cost of coroutine management is greatly reduced.

Instead of using multiple GlobalScope.launch to create job objects like GlobalScope , each job has to be canceled () to close all coroutines.

But like CoroutineScope , because we said

Whether it is GlobalScope.launcha function or launcha function, they will return a Job object , so our first purpose is to create a Job object

Then we pass the job into CoroutineScope() to obtain a CoroutineScope object , and then .launch this object.

Finally, just use job.cancel() to cancel them all.

4.1.1 Why GlobalScope cannot cancel all coroutines at once, but CoroutineScope can

To put it simply, GlobalScope can only create one launch at a time , but CoroutineScope can create multiple launches at a time.

4.2async

Normally we use CoroutineScope.launch but what we get is the Job object

If you want to create a coroutine and obtain its execution results , you need to use asyncfunctions

I didn't particularly understand the meaning of this sentence at first. I thought it was because there was no way to perform calculation operations in launch , but I tried it and it worked.

Only then did I understand

for example

   var job = Job()
        var c = String()
        var coroutineScope = CoroutineScope(job)
        coroutineScope.launch {
            c = "nihao"
        }
        println(job)

The printout is:

JobImpl{
    
    Active}@660a746

But if you use async

fun main00(){
    runBlocking {
        val result = async {
             5+5
        }.await()
        println(result)
    }
}

Then call main00() in Main

Then you will find that it can print out

10

Here we can understand something

4.2.1 Some properties of async

1. The async function must be called in the coroutine scope. It will create a new sub-coroutine and return a Deferred object .

2. The code in the code block will be executed immediately after calling the async function. When the await() method is called, if the code of the code block has not been executed yet, the await() method will block the current coroutine and guide the execution result of the async function to be obtained.

That is, if the code block in the async function has not been executed yet, because of the await() method, the scope of the coroutine where async is located will not end before async is executed. Instead, the runBlocking coroutine will be blocked first and wait for asyncthe function. End after execution is complete

I made two mistakes on this at first.

The first one is that await is not added . Regardless of whether you add await() or not , it will directly generate a Deferred object first , and then the 5+5 operation will be performed in the background and will not be calculated directly. If **await()** is not added, 5+5 may be discarded and not output.

Then because async quickly generated a Deferred object, I thought it would be executed first, so the first thing printed was 10 .

runBlocking {
    
    
    launch {
    
    
        println("哈哈哈")
    }
    val result = async {
    
    
         5+5
    }.await()
    println(result)
}

However, the first thing printed here is

哈哈哈

and then print

10

But my previous understanding is correct. It does generate a Deferred object first, but then it will not print 10 directly, but print it first hahaha

Only when it executes await will it block the coroutine it is in, instead of executing await() directly.

The second is that I did not add runBlocking , which caused my async to run in an environment without coroutine scope , which would make it popular.

4.2.2 Is async serial or parallel?

fun main333()
{
    
    
    val start = System.currentTimeMillis()
    runBlocking {
    
    
        val result1 = async {
    
    
            delay(1000)
            5 + 5
        }.await()

        val result2 = async {
    
    
            delay(1000)
            4 + 6
        }.await()
        println("result is ${
      
      result1+result2}")
        val end = System.currentTimeMillis()
        println("时间 ${
      
      end-start}")
    }
    }

The final printed result is

result is 20
时间 2004

If it is 2004, you can verify one thing. Async is serial and not parallel . If it is parallel , its time must be less than 2000ms.

In the first delay(), it will directly execute the content in result2 instead of waiting all the time.

But what if we use this code

fun main44(){
    
    
        runBlocking {
    
    
            val start = System.currentTimeMillis()
            val deferred1 = async {
    
    
                delay(1000)
                5 + 5
            }

            val deferred2 = async {
    
    
                delay(1000)
                4 + 6
            }
            println("result is ${
      
      deferred1.await() + deferred2.await()}")
            val end = System.currentTimeMillis()
            println("cost: ${
      
      end - start} ms.")
        }
}

The result of our running

result is 20
cost: 1002 ms.

This is parallel

When encountering delay(1000) , it will execute the code in deffered2, and then delay(1000)

Let's take a look at the difference between the two. The main thing is await . In the first serial , we add an await() after each async . But in the second parallel , we only add await() at the end of printing.

When we wrote launch without adding await() before , it was parallel, so to summarize

4.2.2.1 Summary

If you add **await()** after each async , it will be serial

If **await()** is added at the end, it is parallel

Because await() will block the coroutine where async is located and will not continue until the sub-coroutine created by async is executed.

4.2.3withContext simplifies async

I originally thought this was the end of the story.

Unexpectedly, there is also a withContext to simplify async

fun main() {
    
    
    runBlocking {
    
    
        val result = withContext(Dispatchers.Default) {
    
    
            5 + 5
        }
        println(result)
    }
}

After calling the withContext() function, the code in the code block will be executed immediately , and the external coroutine will be suspended . When all the code in the code block is executed , the execution result of the last line will be withContext()returned as the return value of the function .

We can find that the usage of withContext is similar to using async

Let’s first write the code to implement this in async

fun main() {
    
    
    runBlocking {
    
    
        val result = async {
    
    
            5 + 5
        }.await()
        println(result)
    }
}

Let’s compare the similarities between the two

async withContext
It can only be executed under the coroutine scope. It can only be executed under the coroutine scope.
is a suspend function is a suspend function
A Deferred object will be directly generated first. Deferred objects will not be generated
When await exists, running the async code block will block the coroutine outside it. When running the withContext code block, the external coroutine will be suspended . After all the code in the code block is executed, the result of the last line will be returned as the result of the **withContext()** function.

run this code

val result1 = withContext(Dispatchers.Default){
    
    
    5
    10
}
println("${
      
      result1}")

The final printed result is

10

This will return the result of the last line the same as the **higher-order functions with(), run()** mentioned earlier.

There are so many places mentioned above,

The biggest difference between withContext and async is

Dispatchers.Default

This, the withContext() function will force you to specify a thread parameter

Although coroutines are very lightweight threads, and multiple coroutines can run in one thread , this does not mean that we will not open threads.

For example, network requests, we all know that it is a time-consuming operation. In Java, when we handle network requests, we usually open another thread and let it be executed in the new thread, because we know that it is executed in the main thread. Network requests can easily lead to ANR because they are time consuming.

Let’s take a look at how many types Dispatchers gives us

[The external link image transfer failed. The source site may have an anti-leeching mechanism. It is recommended to save the image and upload it directly (img-yYbBS5dd-1686663619434)(../../assets/QQ picture 20230613201644.png)]

There are Default , Main , IO , Unconfined respectively .

Dispatchers.Default It means that a default low concurrency thread strategy will be used. When the code you want to execute is a computationally intensive task, turning on too high concurrency may affect the running efficiency of the task. In this case, you can use Dispatchers.Default. Dispatchers.DefaultThe thread pool used by the scheduler is a shared background thread pool. The number of threads it contains is usually equal to the number of available processors and is used to perform CPU-intensive computing tasks.
Dispatchers.IO It means that a higher concurrency thread strategy will be used . When the code you want to execute is blocking and waiting most of the time**, such as when executing network requests, in order to support a higher number of concurrency**, at this time You can use Dispatchers.IO. Dispatchers.IOThe thread pool used by the scheduler is a thread pool specifically used for IO operations. It usually contains Defaultmore threads than the scheduler and is used to perform IO-intensive tasks, such as network requests, database operations, etc.
Dispatchers.Main It means that the child thread will not be started , but the code will be executed in the Android main thread. However, this value can only be used in Android projects. Pure Kotlin programs will cause errors when using this type of thread parameters. The scheduler executes the coroutine in the main thread of the Android application, which is usually used to update the UI interface. Only available on Android platform
Dispatchers.Unconfined It does not restrict the coroutine to any specific thread or thread pool, but executes the coroutine immediately in the thread that called it, up to the first suspension point. After the first suspension point, Unconfinedthe scheduler will automatically switch the coroutine to the default scheduler and continue executing the remaining part of the coroutine. Dispatchers.UnconfinedThe scheduler does not restrict the coroutine to any specific thread or thread pool. Instead, the scheduler executes the coroutine immediately in the thread that called it, up to the first suspension point. After the first suspension point, Unconfinedthe scheduler will automatically switch the coroutine to the default scheduler and continue executing the remaining part of the coroutine.

As for why CPU-intensive computing tasks are executed in Dispatcher.Default , it is because CPU-intensive computing tasks cannot be performed when concurrency is particularly high.

In the coroutine scope builder we just learned, except for coroutineScopefunctions, all other functions can specify such a thread parameter, but withContext()the function is mandatory to specify , while other functions are optional. .

4.3 Use coroutine (suspendCoroutine function) to simplify the writing of callbacks

Let’s think about Handler’s callback

val handler = object : Handler(Looper.getMainLooper()){
    
    
    override fun handleMessage(msg: Message) {
    
    
        super.handleMessage(msg)
    }
}

You will find that we have created an anonymous inner class Looper.getMainLooper() for Handler's callback.

How many network requests are initiated, how many such anonymous class implementations must be written. Now, Kotlin's coroutines allow us to write it in a simpler way:

1. Must be called in the coroutine scope or suspending function
2. Receives a Lambda expression parameter
3. The main function is to immediately suspend the current coroutine and then execute the code in the Lambda expression in an ordinary thread.
A Continuation parameter will be passed in the parameter list of the Lambda expression. Calling its **resume() method or resumeWithException()** can resume the coroutine.

We use the suspendCoroutine function to simplify

class MainActivity : AppCompatActivity() {
    
    
    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        coroutineScope.launch(Dispatchers.Main) {
    
    
            postToMainThread{
    
    
                var text:TextView = findViewById(R.id.text_22)
                text.setText("年后")
            }
        }
    }
    suspend fun postToMainThread(block: () -> Unit) {
    
    
        return suspendCoroutine {
    
     continuation ->
            Handler(Looper.getMainLooper()).post {
    
    
                block()
                continuation.resume(Unit)
            }
        }
    }
}

I first created a coroutine scope of coroutineScope.launch . Then specified it to be executed on the main thread (because the UI thread needs to be modified), and then

postToMainThread{
    
    xxx}

xxx is the specific implementation inside

Then hang up postToMainThread (if you don't hang up, you can't use the delay methods), and then Handlerexecute the passed in blockfunction in the main thread. After blockthe function execution is completed, we use continuation.resume(Unit)the method to resume the execution of the coroutine and return the result Unitto the coroutine as a return value of type.

Guess you like

Origin blog.csdn.net/m0_61130325/article/details/131196864