Android - Kotlin coroutine Global.launch function

1. Introduction

So what is a coroutine? It is actually somewhat similar to a thread, and it can be simply understood as a lightweight thread. You should know that the threads we have learned before are very heavyweight, and it needs to rely on the scheduling of the operating system to switch between different threads. However, using coroutines can switch between different coroutines only at the programming language level, which greatly improves the operating efficiency of concurrent programming.

To give a specific example, for example, we have the following two methods foo() and bar():

fun foo() {
 a()
 b()
 c()
}
fun bar() {
 x()
 y()
 z()
}

If the two methods of foo() and bar() are called successively without starting the thread, the theoretical result must be that after a(), b(), and c() are executed, x(), y() , z() can be executed. And if you use a coroutine, call the foo() method in coroutine A, and call the bar() method in coroutine B, although they will still run in the same thread, but at any time when the foo() method is executed It is possible to be suspended and turn to execute the bar() method. When executing the bar() method, it may be suspended at any time and turn to continue to execute the foo() method, and the final output result becomes uncertain.

It can be seen that coroutines allow us to simulate the effect of multi-threaded programming in single-threaded mode, and the suspension and recovery of code execution are completely controlled by the programming language and have nothing to do with the operating system. This feature greatly improves the operating efficiency of high-concurrency programs.

2. Use - Global.launch function

Add the following dependency libraries in the app/build.gradle file:

dependencies {
 ...
 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1"
 implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
}

How to start a coroutine? The easiest way is to use the Global.launch function

fun main() {
 GlobalScope.launch {
 println("codes run in coroutine scope")
 }
}

Three, runBlocking function

A function that ends after all code in the coroutine has run

fun main() {
     runBlocking {
         println("codes run in coroutine scope")
         delay(1500)
         println("codes run in coroutine scope finished")
     }
}

The runBlocking function also creates a coroutine scope, but it can ensure that the current thread is blocked until all codes and sub-coroutines in the coroutine scope are executed. It should be noted that the runBlocking function should usually only be used in a test environment, and it is easy to cause some performance problems when used in a formal environment.

Four,

Although we have been able to let the code run in the coroutine now, it seems that we have not experienced any special benefits. This is because all the code currently runs in the same coroutine, and once it involves high-concurrency application scenarios, the advantages of coroutines over threads can be reflected.

fun main() {
     runBlocking {
         launch {
             println("launch1")
             delay(1000)
         println("launch1 finished")
     }
         launch {
             println("launch2")
             delay(1000)
             println("launch2 finished")
         }
     }
}

So how can we create multiple coroutines? Very simple, just use the launch function,

Note that the launch function here is different from the GlobalScope.launch function we just used. First of all, it must be called in the scope of the coroutine, and secondly, it will create sub-coroutines under the scope of the current coroutine. The characteristic of sub-coroutines is that if the coroutine in the outer scope ends, all sub-coroutines under this scope will also end together. In contrast, the GlobalScope.launch function always creates top-level coroutines, which is similar to threads, because threads have no hierarchy and are always top-level.

五、suspend -> coroutineScope

As the logic in the launch function becomes more complex, you may need to extract parts of the code into a separate function. At this time, a problem arises: the code we write in the launch function has a coroutine scope, but when extracted into a separate function, there is no coroutine scope, so how do we call something like delay() What about the suspend function?

For this reason, Kotlin provides a suspend keyword, which can be used to declare any function as a suspended function, and the suspended functions can call each other 

suspend fun printDot() {
     println(".")
     delay(1000)
}

 In this way, the delay() function can be called in the printDot() function.

However, the suspend keyword can only declare a function as a suspended function, and cannot provide it with a coroutine scope. For example, if you try to call the launch function in the printDot() function now, the call must fail, because the launch function must be called in the scope of the coroutine.

This problem can be solved with the help of coroutineScope function. The coroutineScope function is also a suspending function, so it can be called within any other suspending function. Its characteristic is that it will inherit the scope of the external coroutine and create a sub-coroutine. With this feature, we can provide coroutine scope to any suspending function.

suspend fun printDot() = coroutineScope {
     launch {
     println(".")
     delay(1000)
  }
}

As you can see, now we can call the launch function in the printDot() suspend function

In addition, the coroutineScope function is somewhat similar to the runBlocking function , which can ensure that all codes and sub-coroutines in its scope will be suspended until all the sub-coroutines are executed.

fun main() {
     runBlocking {
         coroutineScope {
             launch {
                 for (i in 1..10) {
                     println(i)
                     delay(1000)
                 }
             }
         }
         println("coroutineScope finished")
     }
     println("runBlocking finished")
}

Here, first use the runBlocking function to create a coroutine scope, and then call the coroutineScope function to create a sub-coroutine. In the scope of coroutineScope, we call the launch function to create a sub-coroutine, and print the numbers 1 to 10 in sequence through the for loop, with one second between each printing. Finally, at the end of the runBlocking and coroutineScope functions, another line of logs is printed.

It can be seen that the coroutineScope function really suspends the external coroutine, and only after all the codes and sub-coroutines in its scope are executed, the code after the coroutineScope function can be run.

Although it seems that the functions of the coroutineScope function and the runBlocking function are somewhat similar, the coroutineScope function will only block the current coroutine, neither affecting other coroutines nor any threads, so it will not cause any performance problems. Since the runBlocking function will suspend the external thread, if you happen to call it in the main thread, it may cause the interface to freeze, so it is not recommended to use it in actual projects.

6. More scope builders

GlobalScope.launch, runBlocking, launch, and coroutineScope are scope builders that can all be used to create a new coroutine scope. However, the GlobalScope.launch and runBlocking functions can be called anywhere, the coroutineScope function can be called in the coroutine scope or suspend function, and the launch function can only be called in the coroutine scope.

As mentioned earlier, runBlocking is only recommended for use in test environments because it will block threads. Since GlobalScope.launch creates a top-level coroutine every time, it is generally not recommended to use it, unless you are very clear that you want to create a top-level coroutine.

Why is it not recommended to use top-level coroutines? Mainly because it is too expensive to manage. For example, if we use a coroutine to initiate a network request in an Activity, because the network request is time-consuming, the user closes the current Activity before the server has time to respond, and it should be canceled at this time This network request, or at least should not be called back, because the Activity no longer exists, and it is meaningless to call back.

So how to cancel the coroutine? Whether it is the GlobalScope.launch function or the launch function, they will return a Job object, and you only need to call the cancel() method of the Job object to cancel the coroutine

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

But 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. Just imagine, is such code impossible to maintain at all?

Therefore, GlobalScope.launch, a coroutine scope builder, is not commonly used in actual projects. Let me demonstrate the more commonly used writing methods in actual projects:

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

We first create a Job object, and then pass it into the CoroutineScope() function. Note that CoroutineScope() here is a function, although its name is more like a class. The CoroutineScope() function will return a CoroutineScope object. The design of this grammatical structure is more like we created an instance of CoroutineScope, which may be intentional by Kotlin. With the CoroutineScope object, you can call its launch function at any time to create a coroutine.

Now all 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 cancel() method only once, thus greatly reducing the cost of coroutine management.

However, in contrast, the CoroutineScope () function is more suitable for use in actual projects. If you just write some codes for learning and testing in the main () function, it is most convenient to use the runBlocking function.

seven,

The content of the coroutine is indeed more, and we will continue to learn below. You already know that calling the launch function can create a new coroutine, but the launch function can only be used to execute a piece of logic, but cannot obtain the execution result, because its return value is always a Job object. So is there any way to create a coroutine and get its execution result? Of course, it can be achieved by using async function.

The async function must be called in the scope of the coroutine. It will create a new sub-coroutine and return a Deferred object. If we want to get the execution result of the async function code block, we only need to call the await() method of the Deferred object can

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

In fact, after calling the async function, the code in the code block will start executing immediately. When the await() method is called, if the code in the code block has not been executed, the await() method will block the current coroutine until the execution result of the async function can be obtained.

fun main() {
 runBlocking {
  val start = System.currentTimeMillis()
  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("cost ${end - start} ms.")
 }
}

Async functions run time-consuming in series

It can be seen that the running time of the entire code is 2032 milliseconds, indicating that the two async functions here are indeed in a serial relationship, and the previous one can be executed after the previous one is executed.

But this way of writing is obviously very inefficient, because two async functions can be executed at the same time to improve operating efficiency. Now modify the above code as follows

fun main() {
 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} milliseconds.")
 }
}

Now the running time of the whole code has become 1029 milliseconds, and the improvement of operating efficiency is obvious

Finally: the withContext() function. The withContext() function is a suspending function, which can be roughly understood as a simplified version of the async function. The example is as follows

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 at the same time. When all the code in the code block is executed, the execution result of the last line will be returned as the return value of the withContext() function, so it is basically equivalent to the writing of val result = async{ 5 + 5 }.await(). The only difference is that the withContext() function forces us to specify a thread parameter. Let me talk about this parameter in detail.

Coroutine is a lightweight thread concept. Therefore, in many traditional programming situations, it is necessary to enable concurrent tasks executed by multiple threads. Now it is only necessary to enable multiple coroutines to execute under one thread. But this does not mean that we never need to open threads. For example, Android requires that network requests must be performed in sub-threads. Even if you open a coroutine to execute network requests, if it is a coroutine in the main thread, Then the program will still go wrong. At this time, we should specify a specific running thread for the coroutine through the thread parameter.

Guess you like

Origin blog.csdn.net/m0_59482482/article/details/129954173