What is a coroutine

According to Wikipedia, a Coroutine is a component of a computer program that promotes cooperative multitasking subroutines that allow execution to be suspended and resumed.

Coroutine is not a new word. Marvin Conway invented the term “Coroutine” in 1958 and applied it to assembler programs. Other languages, such as Go and Python, also have the concept of coroutines, so it is not unique to Kotlin.

Coroutines are implemented differently at different language levels, and the Kotlin coroutine described in this article is essentially a lightweight thread.

A Kotlin coroutine runs in a thread, which can be either single-threaded or multi-threaded. Using coroutines in a single thread takes no less time than not using coroutines.

The above are the concepts of coroutines and the characteristics of Kotlin coroutines. So why is there a Kotlin coroutine? How is it better than threads? Let’s move on.

Preliminary understanding of Kotlin coprogram

In Kotlin, a coroutine is a thread wrapper that provides a standard API for writing concurrent tasks. Recall, in Java and Android, how do we write concurrent tasks?

Java implements concurrent multitasking

In Java, we can use threads or thread pools for multitasking concurrency:

// Thread new Thread(new Runnable() {@override public void run() {// time required to work}}).start(); / / thread pool ExecutorService executor = Executors. NewFixedThreadPool (3); Executor.execute (new Runnable() {@override public void run() {// Time required work}});Copy the code

Android enables concurrent multitasking

In Android, in addition to creating threads and using thread pools to achieve multi-task concurrency through Java, AsyncTask can also be used to achieve concurrent execution of multiple time-consuming tasks:

//AsyncTask public abstract class AsyncTask<Params, Progress, Result> { Protected abstract Result doInBackground(Params... params); Protected void onProgressUpdate(Progress... Protected void onPostExecute(result result) {}}Copy the code

Both Java and Android components can implement concurrent multitasking, but the above components have more or less problems:

  • After the execution of a time-consuming task, the child thread needs to pass the result back to the main thread, so the communication between the two is not convenient.
  • AsyncTaskThere are a lot of callback methods to handle, and nesting of callbacks can occur when there are multiple tasks.

Coroutines implement multi-task concurrency

Continuing with AsyncTask for example 🌰 :

AsyncTask<String, Integer, String> task = new AsyncTask<String, Integer, String>() { @Override protected String doInBackground(String... strings) { String userId = getUserId(); Return userId; } @Override protected void onPostExecute(final String userId) { AsyncTask<String, Integer, String> task1 = new AsyncTask<String, Integer, String>() { @Override protected String doInBackground(String... strings) { String name = getUserName(userId); // Get userName, use userId return name; } @Override protected void onPostExecute(String name) { textView.setText(name); // Set to TextView control}}; task1.execute(); // If task1 is a time-consuming task, get userName}}; task.execute(); // Assuming task is a time-consuming task, get the userIdCopy the code

If coroutines are used, the above example can be simplified as:

Globalscope.launch (dispatchers.main) {val userId = getUserId() // Time-consuming tasks, Textview. text = userName // set it to textView control, } suspend fun getUserId(): String = withContext(dispatchers.io) {// Time elapsed operation, return userId} suspend fun getUserName(userId: String): String = withContext(dispatchers. IO) {// Time operation, return userName}Copy the code

The {} logic of the launch function above is a coroutine.

Kotlin coroutines have the following advantages over AsyncTask writing:

  • Coroutines put time-consuming tasks and UI updates in the top three lines, eliminatingAsyncTaskNested callback, use more convenient, concise.
  • Through suspension and recovery, coroutines can directly return the results of time-consuming tasks to the caller, so that the master thread can directly use the results of the child thread, and the UI update is more convenient.

Access and use of Kotlin coroutines

How to access

Add the following dependencies to the build.gradle module:

Dependencies {implementation "org. Jetbrains. Kotlinx: kotlinx coroutines -- core: $1.3.9" implementation "Org. Jetbrains. Kotlinx: kotlinx coroutines - android: 1.3.9"}Copy the code

How to use

Kotlin provides three ways to create coroutines, as follows:

RunBlocking {//runBlocking is a top-level function... Launch {//GlobalScope is a singleton object that launches the coroutine directly... } val coroutineScope = coroutineScope (context) // Create a coroutineScope object with CoroutineContext, Launch coroutinescope.launch {... }Copy the code
  • Method one: it is thread-blocking, and it is usually used in unit tests and main functions, so we don’t usually use it in development.
  • Method 2: Compared to method 1, it does not block threads, but it has the same lifecycle as the application and cannot be cancelled (more on that later), so it is not recommended.
  • Method 3: PassCoroutineContextTo create aCoroutineScopeObject, throughCoroutineScope.launchorCoroutineScope.asyncYou can turn on coroutines and passCoroutineContextYou can also control the life cycle of coroutines. It is generally recommended to start coroutines in this way during development.

CoroutineContext

Globalscope. launch or Coroutinescope. launch. The first argument to the launch method is CoroutineContext.

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

One of the functions of the context, CoroutineContext, is to perform thread switching, that is, the coroutine body will run in the specified thread represented by CoroutineContext.

The Kotlin coroutine officially defines several values that we can use during development. They are:

  • Dispatchers.Main

The coroutine body will run on the main thread for UI updates and other scenarios that need to be executed on the main thread, as you probably know.

  • Dispatchers.IO

The coroutine body will run on IO threads for IO intensive operations such as network requests, file operations, and other scenarios.

  • Dispatchers.Default

The coroutine body will run on the Default thread, if the context is not specified or if it is dispatchers.default. CPU intensive scenarios, such as those involving a large amount of computing. Note that the default thread shares the same thread pool as the IO thread above.

  • Dispatchers.Unconfined

Unrestricted scheduler, should not be used in development, not yet investigated.

Take a look at this example:

GlobalScope.launch(Dispatchers.Main) { println("Main Dispatcher, currentThread=${Thread.currentThread().name}") } GlobalScope.launch { println("Default Dispatcher1, currentThread=${Thread.currentThread().name}") } GlobalScope.launch(Dispatchers.IO) { println("IO Dispatcher, currentThread=${Thread.currentThread().name}") } GlobalScope.launch(Dispatchers.Default) { println("Default Dispatcher2,  currentThread=${Thread.currentThread().name}") }Copy the code

The running results of the program are as follows:

As you can see, the coroutines of Dispatchers.Main run on the Main thread, while the coroutines of Dispatchers, Dispatchers.IO, Dispatchers.

Launch and async

We mentioned above that you can use launch to create a coroutine, but in addition to using launch, Kotlin provides Async to help us create coroutines. The difference between the two is:

  • Launch: Create a coroutine and return oneJob, but does not carry the result of coroutine execution.
  • Async: Creates a coroutine and returns oneDeferred(also a Job), and carries the result of coroutine execution.

The Deferred returned by Async is a lightweight non-blocking future that represents a promise that will provide the result later, so it needs to use the await method to get the final result. Here’s an example of async from Kotlin:

val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } println("The answer is ${one.await() + two.await()}") suspend fun doSomethingUsefulOne(): Int {suspend fun doSomethingUsefulTwo()} suspend fun doSomethingUsefulTwo(): Int {delay(1000L) // Assuming we do something useful here too return 29}Copy the code

After executing the above code, the result is:

The answer is 42
Copy the code

Usage scenarios for Kotlin coroutines

thread

In the CoroutineContext section, the coroutine in the example was still running in a single thread. In actual development, a common scenario is thread switching and recovery, which requires the withContext method.

withContext

Let’s continue with the example from the comparison with Threads section:

Globalscope.launch (dispatchers.main) {val userId = withContext(dispatchers.io) {getUserId() Textview. text = userId // Set to textView control, switch to main thread}Copy the code

Here is a typical network request scenario: it starts on the main thread, then needs to go to the background to get the value of the userId (here the getUserId method is executed), completes the fetch, returns the results, switches back to the main thread, and finally updates the UI control.

When the userId is obtained, the getUserId method is called. In this case, the withContext method is used to switch the thread from main to IO. When the time-consuming task is completed (i.e. the getUserId method above is completed), The other thing withContext does is it reverts back to the thread where it was before the child thread was switched, which is the main thread in the example above, so we can update the UI control.

We can also manage the withContext logic in a separate method, as follows:

Globalscope.launch (dispatchers.main) {val userId = getUserIdAsync() textView.text = userId // set to textView control, Fun getUserIdAsync() = withContext(dispatchers.io) {getUserId() // Time-consuming task, here will switch to child thread}Copy the code

This looks like a synchronous invocation of asynchronous logic, but if you do this, the IDE will report an error with the following message: Suspend function’withContext’ should be called only from a coroutine or another Suspend funcion.

Meaning that withContext is a suspend method that needs to be called in either a coroutine or another suspend method.

suspend

Suspend is a keyword for the Kotlin coroutine, which means “suspend.” The error can be resolved by adding the suspend keyword.

Globalscope.launch (dispatchers.main) {val userId = getUserIdAsync() textView.text = userId // set to textView control, } suspend fun getUserIdAsync() = withContext(dispatchers.io) {getUserId()}Copy the code

When code executes to the suspend method, the current coroutine is suspended, which is non-blocking, meaning that it does not block the current thread. This is known as a “non-blocking suspend.”

Non-blocking suspend and coroutine execution steps

One of the prerequisites for a non-blocking suspend is that multithreaded operations must be involved. Because the concept of blocking is specific to a single thread. When we switch the thread, it must be non-blocking, because the time-consuming operation goes to another thread, and the original thread is free to do whatever it wants

If multiple coroutines are started in the main thread, what is the order in which the coroutines are executed? Is it in code order? Or is there another order of execution? Suppose the test method is executed in the main thread, as shown in the following code. What should this code output?

Fun test() {println("start test fun, Thread =${thread.currentThread ().name}") // A globalscope.launch (dispatchers.main) {println("start coroutine1, thread=${Thread.currentThread().name}") val userId = getUserIdAsync() println("end coroutine1, Thread =${thread.currentThread ().name}")} thread=${Thread.currentThread().name}") delay(100) println("end coroutine2, thread=${Thread.currentThread().name}") } println("end test fun, thread=${Thread.currentThread().name}") } suspend fun getUserIdAsync() = withContext(Dispatchers.IO) { println("getUserIdAsync, thread=${Thread.currentThread().name}") delay(1000) return@withContext "userId from async" }Copy the code

Running the above code in Android results in:

As can be seen from the printed log, although the code order of the coroutine is in println(” End test fun…” ) before, but in order of execution, the coroutine is still started in println(” End test fun…” ), combined with non-blocking suspend, the following figure shows the sequential flow of coroutines:

Reference documentation

1. A hard glance at Kotlin’s coroutines – Can you learn coroutines? Probably because the tutorials you read are all wrong

2. Introduction to coroutines

3. The most comprehensive Kotlin coroutine