coroutine
Article directory
- coroutine
- 1. Let’s talk about some related concepts before coroutines
- 2.Coroutine
- 3.Coroutine scope
- 4. Scope constructor
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
- 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. - 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 Looper
blocking the main thread when no message is received, it will not cause ANR. However, if Handler
time-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 Looper
the infinite loop, it will continuously take out messages from the message queue and distribute the messages to the corresponding Handler
processing. When the message queue is empty, Looper
it will wait in a loop for new messages to arrive.
While waiting for messages, Looper
the 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.
- 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.
- 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 runBlocking
to 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 runBlocking
to 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.launch
a function or launch
a 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.launch
a function or launch
a 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 async
functions
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 async
the 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
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.Default The 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.IO The thread pool used by the scheduler is a thread pool specifically used for IO operations. It usually contains Default more 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, Unconfined the scheduler will automatically switch the coroutine to the default scheduler and continue executing the remaining part of the coroutine. |
Dispatchers.Unconfined The 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, Unconfined the 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 coroutineScope
functions, 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 Handler
execute the passed in block
function in the main thread. After block
the function execution is completed, we use continuation.resume(Unit)
the method to resume the execution of the coroutine and return the result Unit
to the coroutine as a return value of type.