Kotlin coroutines

1. 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 multi-threaded programming in single-threaded modeThe effect of the program, the suspension and recovery of the code execution is completely controlled by the programming language, and has nothing to do with the operating system. This feature has greatly improved the operating efficiency of high-concurrency programs. Just imagine, opening 100,000 threads is completely unimaginable, right? It is completely feasible to enable 100,000 coroutines, and we will verify this function later.

2. Basic usage

2.1 Import dependent library

Kotlin does not incorporate coroutines into the API of the standard library, but provides them in the form of dependent libraries. So if we want to use the coroutine function, we need to add the following dependency library in the app/build.gradle file first:

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

The second dependency library is only used in Android projects. The code examples we write in this section are all pure Kotlin programs, so the second dependency library is not practical. But in order not to explain it separately next time when using coroutines in an Android project, it is introduced here together.

2.2 Create CoroutinesTest.kt file

Next, create a CoroutinesTest.kt file, define a main() function, and start our coroutine journey. First of all, the first question we have to face is, how to start a coroutine? The easiest way is to use the Global.launch function, as follows:

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

The GlobalScope.launch function can create a coroutine scope, so that the code block (Lambda expression) passed to the launch function is run in the coroutine. Here we just print a line of logs in the code block. So now run the main() function, can the log be printed successfully? If you try it, there is no log output.

This is because the Global.launch function creates a top-level coroutine each time, and this coroutine will also end together when the application ends. The reason why the log just now cannot be printed out is because the code in the code block has not had time to run, and the application ends.

It is also very simple to solve this problem, we just let the program end after a delay, as shown below:

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

The Thread.sleep() method is used here to block the main thread for 1 second. Now run the program again, and you will find that the log can be printed normally, as shown in Figure 11.11.

However, there are still problems with this way of writing. If the code in the code block cannot finish running within 1 second, it will be forcibly interrupted. Observe the following code:

fun main() {
    GlobalScope. launch {
        println("codes run in coroutine scope")
        delay(1500)
        println("codes run in coroutine scope finished")
    }
    Thread. sleep(1000)
}

We added a delay() function in the code block, and then printed a line of log. The delay() function allows the current coroutine to run after a specified time delay, but it is different from the Thread.sleep() method. The delay() function is a non-blocking suspending function, which only suspends the current coroutine and does not affect the operation of other coroutines. The Thread.sleep() method will block the current thread, so that all coroutines running under this thread will be blocked. Note that the delay() function can only be called within the scope of a coroutine or other suspending functions.

Here we let the coroutine hang for 1.5 seconds, but the main thread is only blocked for 1 second, what will be the final result? Re-run the program, and you will find that a new log in the code block is not printed out, because the application has ended before it can run.

So is there any way to make the application end after all the code in the coroutine has run? Of course, there are, and this function can be realized with the help of the runBlocking function:

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

The runBlocking function will also create a coroutine scope, but it can guarantee that the current thread will be blocked until all the codes and sub-coroutines in the coroutine scope are executed. requires attentionUnfortunately, 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.

The result is shown in the figure:

As you can see, both logs can be printed normally.

2.3 Create multiple coroutines

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.

So how can we create multiple coroutines? It’s very simple, just use the launch function, as shown below:

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

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. By comparison, The GlobalScope.launch function always creates a top-level coroutine, which is similar to a thread, because threads have no hierarchy and are always top-level.

Operation result:

It can be seen that the logs in the two sub-coroutines are printed alternately, indicating that they are indeed running concurrently like multiple threads. However, these two sub-coroutines actually run in the same thread, and it is only up to the programming language to decide how to schedule among multiple coroutines, who is allowed to run and who is allowed to suspend. The scheduling process does not require the participation of the operating system at all, which makes the concurrency efficiency of coroutines surprisingly high.

So how high will it be? Let’s do the experiment and we will know, the code is as follows:

fun main() {
    val start = System. currentTimeMillis()
    runBlocking {
        repeat(100000){
            launch {
                println(".")
            }
        }
    }
    val end = System. currentTimeMillis()
    println(end-start)
}

Here, 100,000 coroutines are created using the repeat function loop, but no meaningful operations are performed in the coroutines, just a symbolic point is printed, and then the running time of the entire operation is recorded. Now run the program again, the result is as shown in the figure:

As you can see, it only took 1207 milliseconds here, which is enough to prove how efficient the coroutine is. Just imagine, if 100,000 threads are turned on, the program may have an OOM exception.

However, as the logic in the launch function becomes more and more complex, you may need to extract part 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 suspend function, and the suspend functions can call each other, as shown below:

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. An example is written as follows:

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. Let’s look at the following sample code:

fun main() {
    runBlocking {
        coroutineScope {
            launch {
                for (i in 1..10){
                    println(i)
                    delay(1000)
                }
            }
        }
        println("coroutineScope finished")
    }
    println("run Blocking 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. Now run the program again, the result is as shown in the figure:

You will see that the console will output the numbers 1 to 10 at intervals of 1 second, and then print the log at the end of the coroutineScope function, and finally print the log at the end of the runBlocking function.

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 coroutineScope function and runBlocking function are somewhat similar, coroutineScope function will only block the current coroutine, neither affecting other coroutines nor any thread , so will not cause any performance issues. 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 not recommended in Used in actual projects.

2.4 More scope builders

In the previous section, we learned about scope builders such as GlobalScope.launch, runBlocking, launch, and coroutineScope, all of which can 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, because runBlocking will block threads, it is only recommended to be used in a test environment. 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.

forWhy 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. You only need to call the cancel() method of the Job object to cancel the coroutine, as shown below:

 val job = GlobalScope. launch {
        //Process specific logic
    }
    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 {
        //Process specific logic
    }
    job. cancel()

As you can see, we first created a Job object, and then passed it into the CoroutineScope() function, pay attention to the Coroutine hereScope() is a function, although it’s named 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.

In contrast, the CoroutineScope() function is more suitable for 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.

The content of the coroutine is indeed quite a lot, 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.

asyncThe function must be called in the coroutine scope, 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, Just call the await() method of the Deferred object, the code is as follows:

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

But the mysteries of async functions don’t stop there. 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.

In order to prove this, we write the following code to verify:

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.")
    }
}

The result of the operation is as follows:

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

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 result1 = async {
            delay(1000)
            5 + 5
        }
        val result2 = async {
            delay(1000)
            4 + 6
        }
        println("result is ${result1.await() + result2.await()}")
        val end = System. currentTimeMillis()
        println("cost ${end - start} ms.")
    }
}

Now we don’t use the await() method to get the result immediately after calling the async function, but only call the await() method to get the result when we need to use the execution result of the async function, so that the two async functions It becomes a parallel relationship.

Finally, let’s learn a special scope builder: withContext() function. The withContext() function is a suspending function, which can be roughly understood as a simplified version of the async function,Example written 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 method of val result =async{ 5 + 5 }.await(). The only difference is that the withContext() function forces us to specify a thread parameter, and I am going to 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.

Thread parameters mainly have the following three values: Dispatchers.Default, Dispatchers.IO and Dispatchers.Main. Dispatchers.Default indicates 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. You can use it at this time Dispatchers.Default. Dispatchers.IO indicates that it will use a higher concurrency thread strategy. When the code you want to execute is blocked and waiting most of the time, such as executing network requests, in order to support higher The number of concurrency, you can use Dispatchers.IO at this time. Dispatchers.Main means that the sub-thread will not be opened, but the code will be executed in the Android main thread, but this value can only be used in the Android project, a pure Kotlin program using this type of thread parameter will cause an error.

2.5 Using Coroutines to Simplify Callback

Learned the callback mechanism of the programming language, and used this mechanism to realize the function of obtaining asynchronous network request data response. I don’t know if you have noticed that the callback mechanism is basically implemented by anonymous classes, but the writing of anonymous classes is usually cumbersome, such as the following code:

How many places to initiate network requests, you need to write such anonymous class implementations as many times. This can’t help but arouse our thinking, is there a simpler way to write it? In the past, there may really have been no simpler way to write it. But now, Kotlin’s coroutines make our idea possible. We only need to use the suspendCoroutine function to greatly simplify the writing of the traditional callback mechanism. Let’s learn about it in detail.

suspendCoroutine must be called in coroutine scope or suspend function, it receives a Lambda expression parameter, the main function It is immediately suspend the current coroutine, and then execute the code in the Lambda expression in a common thread. Lambda expressionA Continuation parameter will be passed in the parameter list of the formula, and its resume() method or resumeWithException() can be called to resume the execution of the coroutine.

After understanding the function of the suspendCoroutine function, we can then use this function to optimize the traditional callback writing method. First define a request() function, the code is as follows:

As you can see, the request() function is a suspending function and receives an address parameter. Inside the request() function, we call the suspendCoroutine function just introduced, so that the current coroutine will be suspended immediately, and the code in the Lambda expression will be executed in a normal thread. Then we call the HttpUtil.sendHttpRequest() method in the Lambda expression to initiate a network request, and monitor the request result through the traditional callback method. If the request is successful, call the resume() method of the Continuation to resume the suspended coroutine, and pass in the server response data, which will become the return value of the suspendCoroutine function. If the request fails, call resumeWithException() of the Continuation to resume the suspended coroutine, and pass in the specific reason for the exception.

You might say, isn’t the traditional callback method still used here? How did the code become more simplified? This is because, no matter how many network requests we have to make in the future, there is no need to repeat the callback implementation. For example, to obtain the response data of Baidu’s homepage, you can write like this:

Because getBaiduResponse() is a suspending function, Therefore, when it calls the request() function, the current coroutine will be suspended immediately, and then the current coroutine will resume running after waiting for the success or failure of the network request. In this way, we can get the response data of the asynchronous network request even without using the callback method, and if the request fails, it will directly enter the catch statement.

In fact, the suspendCoroutine function can be used to simplify the writing of almost any callback. For example, before using Retrofit to initiate a network request, it needs to be written like this:

Because the data types returned by different Service interfaces are also different, so this time we can’t program for specific types like just now, but use generic methods. Define an await() function, the code is as follows:

This code is a little more complicated than the request() function just now. First, the await() function is still a suspending function, and then we declare a generic T for it, and define the await() function as an extension function of Call, so that all Retrofit network request interfaces whose return value is Call type are You can call the await() function directly.

Next, the await() function uses the suspendCoroutine function to suspend the current coroutine, and because of the extension function, we now have the context of the Call object, so we can directly call the enqueue() method here to let Retrofit initiate network request. Next, use the same method to process the data responded by Retrofit or the failure of the network request. Another point to note is that in the onResponse() callback, we call the object parsed by the body() methodis possibly empty. If it is empty, the method here is to manually throw an exception, and you can also handle it more appropriately according to your own logic.

With the await() function, it becomes extremely simple for us to call all Retrofit Service interfaces. For example, the same function just now can be implemented using the following writing method:

Without the lengthy implementation of anonymous classes, you only need to simply call the await() function to allow Retrofit to initiate a network request and directly obtain the data that the server responds to. Do you think the code has become extremely simple? Of course, you may think that it is troublesome to perform a try catch process every time a network request is initiated. In fact, we can choose not to process it here. If it is not handled, if an exception occurs, it will be thrown up layer by layer until it is processed by a function of a certain layer. Therefore, we can also perform only one try catch in a unified entry function, so that the code becomes more streamlined.

Leave a Reply

Your email address will not be published. Required fields are marked *