The author

Hello everyone, my name is Xiaoqi;

I graduated from The Software Engineering major of Central South University of Forestry and Technology in 2016. After graduation, I worked as an Android developer in the education industry. Later, I joined the android team of 37 mobile games in October, 2019.

At present, I am mainly responsible for the development of domestic release of Android, as well as the development of several internal apps.

Some of the concepts

Before we look at coroutines, let’s review the concept of threads, processes

1. Process: has code and open file resources, data resources, independent memory space, is the smallest unit of resource allocation.

2. Thread: subordinate to the process, is the actual execution of the program, a process contains at least one thread, operating system scheduling (CPU scheduling) the smallest unit of execution

3. Coroutines:

  • Not managed by the operating system kernel, but completely controlled by programs, that is, executed in user mode
  • Processes and threads are operating system dimension, coroutines are language dimension.

Coroutines feature

  • Asynchronous code synchronization

Here is an example to experience this feature of coroutines in Kotlin

Here is a scenario where a network interface is requested to retrieve user information and then update the UI to display the user information using Kotlin’s coroutine:

GlobalScope.launch(Dispatchers.Main) {   // start the coroutine in the main thread
    val user = api.getUser() // The IO thread executes the network request
    tvName.text = user.name  // Main thread updates UI
}
Copy the code

To implement this logic in Java, we usually write:

api.getUser(new Callback<User>() {
    @Override
    public void success(User user) {
        runOnUiThread(new Runnable() {
            @Override
            public void run(a) { tvName.setText(user.name); }})}@Override
    public void failure(Exception e) {... }});Copy the code

This kind of asynchronous callback in Java disrupts the normal code order and, while logically sequential, makes reading very uncomfortable. If there are more concurrent scenarios, there will be “callback hell”. With Kotlin coroutines, multi-layer network requests need only be written like this:

GlobalScope.launch(Dispatchers.Main) {       // Start coroutine: main thread
    val token = api.getToken()                  // Network request: IO thread
    val user = api.getUser(token)               // Network request: IO thread
    tvName.text = user.name                     // Update UI: main thread
}
Copy the code

As you can see, even relatively complex parallel network requests can be written with well-defined code through coroutines

Initial experience of coroutine

1. Introduce dependencies

    implementation 'org. Jetbrains. Kotlinx: kotlinx coroutines -- core: 1.4.0'
    implementation 'org. Jetbrains. Kotlinx: kotlinx coroutines - android: 1.4.0'
Copy the code

2. The first coroutine

Add a button to the layout and set the click event for it

btn.setOnClickListener {
    Log.i("TAG"."1. Prepare to start coroutine.... [Current thread is:${Thread.currentThread().name}]. "")
    CoroutineScope(Dispatchers.Main).launch{
        delay(1000)     / / 1000 ms delay
        Log.i("TAG".2. Perform CoroutineScope... [Current thread is:${Thread.currentThread().name}]. "")
    }
    Log.i("TAG"."3.BtnClick.... [Current thread is:${Thread.currentThread().name}]. "")}Copy the code

The result is as follows:

1. Prepare to start coroutine.... [Current thread: main] 3.BtnClick.... [The current thread is main] 2. Run CoroutineScope.... [Current thread: main]Copy the code

A coroutine is started with the coroutinescope.launch method, and the code in curly braces after launch is the code running inside the coroutine. After the coroutine is started, the task in the coroutine body suspends and the code behind coroutinescope.launch continues until the method in the coroutine completes and then automatically cuts back.

Go to the launch method and look at its parameters,

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope. () - >Unit
): Job {
}
Copy the code

Description of these parameters:

  • Context: A coroutine context in which you can specify that the coroutine is restricted to a particular thread. Common ones are Dispatchers.Default, Dispatchers.Main, Dispatchers.IO, etc. Dispatchers.Main is the Main thread in Android; Dispatchers.IO: Optimized for disk and network IO for IO intensive tasks such as reading and writing files, manipulating databases, and network requests
  • Start: indicates the start mode of the coroutine. The DEFAULT (which is the most commonly used) CoroutineStart. DEFAULT refers to coroutines executed immediately, plus CoroutineStart. LAZY, CoroutineStart. ATOMIC, CoroutineStart. UNDISPATCHED
  • Block: The body of the coroutine, the code to run inside the coroutine, which is the code in curly braces in the example above
  • Return value Job: a reference to the coroutine currently created. You can control the start and cancellation of coroutines by calling its join, cancel, and other methods.

3. Suspend functions

The concept of “suspend,” mentioned above,

To return to the above example, there is a delay function. Step into this function to see its definition:

public suspend fun delay(timeMillis: Long) {... }Copy the code

Found a suspend key words, namely “hang” mentioned in the article, according to the results of the program’s output, output the first 1, 3, wait a second, then the output by 2, and print the thread displayed is the main thread, this shows that coroutines in suspend key words, will be suspended, the so-called hangs, When a program cuts a thread, it automatically cuts back when the suspension function is finished. The action of cutting back is actually recovery, so suspension and recovery are also a feature of coroutines. Therefore, the suspension of a coroutine can be understood as the process of the code leaving the coroutine thread, and the recovery of a coroutine can be understood as the process of the code re-entering the coroutine thread. Coroutines switch threads through this suspension recovery mechanism.

There is also a rule about the suspend function: the suspend function must be called from a coroutine or another suspend function. In other words, the suspend function must be executed directly or indirectly from the coroutine.

4. Other ways to create coroutines

When the suspend function is executed, the coroutine will automatically escape from the current thread to perform its task. At this point, the original thread will continue to do its work. When the suspend function is completed, the coroutine will automatically cut back to the original thread and continue to proceed. However, if the thread in which the coroutine is running has ended, execution will not continue until the coroutine has completed. To avoid this situation, you need to use runBlocking to temporarily block the current thread and keep the code in order.

Now let’s create a coroutine with runBlocking

btn.setOnClickListener {
            Log.i("TAG"."1. Prepare to start coroutine.... [Current thread is:${Thread.currentThread().name}]. "")
            runBlocking {
                delay(1000)     / / 1000 ms delay
                Log.i("TAG".2. Perform CoroutineScope... [Current thread is:${Thread.currentThread().name}]. "")
            }
            Log.i("TAG"."3.BtnClick.... [Current thread is:${Thread.currentThread().name}]. "")}Copy the code

The result is as follows:

1. Prepare to start coroutine.... [The current thread is main] 2. Run CoroutineScope.... [Current thread: main] 3.BtnClick.... [Current thread: main]Copy the code

As you can see, the order of launch execution is different from that of launch above, where log outputs 1, 2, and then 3. The program will wait for the code block in runBlocking to complete execution before executing the following code. Therefore, launch is non-blocking, while runBlocking is blocking.

Launch and runBlocking do not return the result, sometimes we want to know the result of coroutines to do business such as UI update, when withContext and async come in handy.

Let’s take a look at the use of withContext:

 btn.setOnClickListener {
            CoroutineScope(Dispatchers.Main).launch {
                val startTime = System.currentTimeMillis()
                val task1 = withContext(Dispatchers.IO) {
                    delay(2000)
                    Log.i("TAG"."1. Perform task1... [Current thread is:${Thread.currentThread().name}]. "")
                    1  // Assign the result to task1
                }

                val task2 = withContext(Dispatchers.IO) {
                    delay(1000)
                    Log.i("TAG".2. Perform task2... [Current thread is:${Thread.currentThread().name}]. "")
                    2 // Assign the result to task2
                }
                Log.i(
                    "TAG"."3. Calculate task1+task2 =${task1+task2}And time consuming${System.currentTimeMillis() - startTime}Ms [Current thread is:${Thread.currentThread().name}]. "")}}Copy the code

The output is:

1. Perform task1... [Current thread: defaultDispatcher-worker-3] 2. Perform task2... [Current thread is defaultDispatcher-worker-1] 3. Task1 +task2 = 3, 3032 msCopy the code

As you can see from the output, the coroutine is run on an IO thread with withContext, and after a delay of two seconds, result 1 is returned and assigned to Task1, after which the program is executed downward. Again, result 2 is returned and assigned to Task2 after a delay of one second. Finally, step 3 is executed and the elapsed time is printed, as you can see, Time is the sum of the time of two tasks, i.e. task1 is executed before task1 is executed, indicating that withContext is executed sequentially. This applies to scenarios where the result of one request depends on the result of another request.

If you are processing multiple time-consuming tasks at the same time and none of the tasks are interdependent, you can use async… Await (), change the above example to async to implement as follows

btn.setOnClickListener {
            CoroutineScope(Dispatchers.Main).launch {
                val startTime = System.currentTimeMillis()
                val task1 = async(Dispatchers.IO) {
                    delay(2000)
                    Log.i("TAG"."1. Perform task1... [Current thread is:${Thread.currentThread().name}]. "")
                    1  // Assign the result to task1
                }

                val task2 = async(Dispatchers.IO) {
                    delay(1000)
                    Log.i("TAG".2. Perform task2... [Current thread is:${Thread.currentThread().name}]. "")
                    2 // Assign the result to task2
                }

                Log.i(
                    "TAG"."3. Calculate task1+task2 =${task1.await()+task2.await()}And time consuming${System.currentTimeMillis() - startTime}Ms [Current thread is:${Thread.currentThread().name}]. "")}}Copy the code

Output result:

2. Perform task2... [Current thread: defaultDispatcher-worker-4] 1. Perform task1... [Current thread is defaultDispatcher-worker-5] 3. Task1 +task2 = 3;Copy the code

As can be seen, the total output time is significantly shorter than that of withContext, and task2 takes precedence over Task1, indicating that async is executed in parallel.

conclusion

This paper based on the process, thread, the concept of the difference between coroutines identify coroutines, then is the advantage of the properties of coroutines are introduced, finally, several examples are introduced coroutines several startup mode, and analyzes their respective characteristics and usage scenarios, this article is more on the concept and use of coroutines simply introduced, and the content of coroutines far more than these.

conclusion

Students who have problems or need to communicate in the process can scan the TWO-DIMENSIONAL code to add friends, and then enter the group for problems and technical exchanges, etc.