Kotlin Coroutine coroutines

  • What is a Kotlin Coroutine? The concept of a Coroutine has been around since the 1960s. Some say it is the release and recovery of control flow, others say it can process concurrently like threads without blocking, and officially it is lighter than threads. We don’t need to deify Kotlin Coroutine. What is it?

Feelings from reading documents, using and reading source code in the last year:

  • Running in a thread, actually running in a thread pool.
  • Because it is running in a thread, it will still block if it is used improperly, such as usingsleep(), or deadlock.
  • Let the coroutines andnew Thread()It is not fair to compare the performance cost of new threads to make them appear lighter. I think it should be withExecutors.newCachedThreadPool()It’s appropriate to compare talents.
  • Makes it easy to write asynchronous code, especially when multiple asynchrons are working simultaneously.
  • With the asynchronous code simpler, we can take the UI thread out of the box and use more asynchronous styles, optimizing UI performance.
  • Switching between UI and IO threads is very convenient.
  • With fewer callbacks and broadcasts to handle asynchrony, the code is easier to read.

The first Kotlin Coroutine

Implement a function that requests user information from the network and displays user nicknames on the UI: Implement the first

GlobalScope.launch(Dispatchers.Main) {
    val userInfo = getUserFromNetwork(userId)// Network requests run in the background
    textView.text = userInfo.name// Update the UI to run on the main thread
}

suspend fun getUserFromNetwork(userId: String): String {
    ...  // The implementation is ignored here, which will be analyzed later
}
Copy the code

See from the coroutine example above:

  • The network request and UI are written in a sequence of methods with no callbacks
  • The two lines of logic are running in different threads
  • getUserFromNetwork()There aresuspendThis modifier
  • GlobalScope.launchThis method is used to start a coroutine

The dilemma of a callback

For example, you need to display the user’s online status and rank in a list.

// The backend API needs to be requested over the network
api.getUserInfo(userId)//1. Query user information
api.getOnlineStatus(userId)//2. Query the online status
api.getLevelInfo(userId)//3. Query the level
Copy the code

There are no dependencies between the three apis, and the most logical approach would be for all three to concurrently request reassembly data.

But with callbacks, it becomes difficult to implement this logic, and the tradeoff is to write it as a serial execution within the callback, which triples the latency across the network.

A serial implementation of a callback

api.getUserInfo(userId, object : Callback<UserInfo> {
    override funOnResponse (the response: the response<UserInfo>) {
      val userInfo = response.body
      api.getOnlineStatus(userId, object: Callback<OnlineStatus> {
        override funOnResponse (the response: the response<OnlineStatus>) {
          val onlineStatus = response.body
          api.getLevelInfo(userId, object: Callback<LevelInfo> {
            override funOnResponse (the response: the response<LevelInfo>) {
              val levelInfo = response.body
              val composeInfo = compose(userInfo, onlineStatus, levelInfo)// Assemble the data
              getLiveData().postValue(composeInfo)/ / refresh the UI}}}}})Copy the code

Parallel implementation of Coroutine

What does the coroutine do to implement the above business logic?

GlobalScope.launch {
  Async (async, async, async, async, async, async, async, async)
  val userInfo = async { api.getUserInfo(userId) }
  val onlineStatus = async { api.getOnlineStatus(userId) }
  val levelInfo = async { api.getLevelInfo(userId) }
  // Wait for all three requests to return before assembling the data
  val composeInfo = compose(userInfo.await(), onlineStatus.await(), levelInfo.await())
  getLiveData().postValue(composeInfo)/ / refresh the UI
}
Copy the code

We can make multithreading work in a sequential way, switching between synchronous and asynchronous. This feature allows us to write logic that would have been difficult to do before, which is an advantage of Coroutine.

  • useasyncTo start a new coroutine @userInfo and return oneDeferred, the callDeferred.await()Method, the current coroutine hangs until @userInfo is executed.

There are two ways to launch coroutines: launch and async. However, there is another way called runBlocking, which is also written in the official document. Meanwhile, it is found that some students may have some misunderstandings about the use of this way. The current thread will be blocked when this runBlocking() is run. Note that this method should not be used inside a Coroutine. According to the official documentation, it is used when blocking the main thread and when testing. It is not recommended.

runBlocking { api.getUserFromNetwork(userId) api.getOnlineStatus(userId) } ... // getUserFromNetwork will not be executed until getUserFromNetwork is finishedCopy the code

Running on a different thread in Coroutine

Here is a full version of the first example:

GlobalScope.launch(Dispatchers.Main) {
    val userInfo = getUserFromNetwork(userId)// Network requests run in the background
    textView.text = userInfo.name// Update the UI to run on the main thread
}

suspend fun getUserFromNetwork(userId: String) = withContext(Dispatchers.IO) {
    HttpSerivce.getUser(userId)
}
Copy the code

withContext

Coroutine has a function withContext() that specifies the thread in which the Coroutine will execute, and makes subsequent code wait to execute in order.

With this function, you can eliminate the nesting of callbacks that occurs when threads are switched.

If we switched threads by starting different coroutines, the code would look like this:

GlobalScope.launch(Dispatchers.IO) { ... launch(Dispatchers.Main) { ... launch(Dispatchers.IO) { ... }}}Copy the code

Is there a sense of a pullback back

Using withContext() lets coroutines get rid of the nesting above.

GlobalScope.launch(Dispatchers.Main) {
    valresult0 = withContext(Dispatchers.IO) {... }valresult1 = withContext(Dispatchers.Main) {... }val result2 = withContext(Dispatchers.IO) {...}
}
Copy the code

This needs to be distinguished from the previous case of concurrent requests, and the scenarios used are different.

Suspend (suspended)

GlobalScope.launch(Dispatchers.Main) {
    val userInfo = getUserFromNetwork(userId)// Network requests run in the background
    textView.text = userInfo.name// Update the UI to run on the main thread
}

suspend fun getUserFromNetwork(userId: String) = withContext(Dispatchers.IO) {
    HttpSerivce.getUser(userId)
}
Copy the code

The logic in suspendGetUserInfo() can be decided not by the caller, but by the implementer.

When we design our own suspend function, if we need to specify it in a particular thread, the best way is to specify it inside our function. For example, when operating the logic of reading and writing files, the definitions are run in Dispatchers.IO, so there is no need to worry about using errors outside.

What is hanging? !

  • Neither the function nor the thread is suspended, but the current coroutine is suspended, and the thread that is running the coroutine will no longer execute the coroutine from the moment it is suspended.
  • When the coroutine reaches the point where the suspended function is executed, it breaks away from the thread that ran it, and the thread doesn’t block, it does something else.
  • When a coroutine breaks away, it does not stop, but waits for the system to arrange for other threads to run it at the appropriate time.
launch(Dispatchers.Main) {
    val userInfo = suspendGetUserInfo(userId)
    textView.text = userInfo.name
}
suspend fun suspendGetUserInfo(userId: Long): UserInfo {
  // The switchable thread is the IO thread. The Main thread will be idle to perform other work
  return withContext(Dispatchers.IO) {
    getUserFromNetwork(userId)
  }
}
Copy the code
// Execute in the Main UI thread
log.debug("run in click starting")
GlobalScope.launch(Dispatchers.Main) {
    log.debug("run in launch")
    val userInfo = suspendGetUserInfo(userId)
}
log.debug("run in click finishing")
Copy the code

What is the order of printing?

The results

D: 11:50:30. 825, main: runinClick starting D: 11:50:30.869 main: runinClick Finishing D: 11:50:30.872 main: RuninLaunch D: 11:50:30.873 main: runin suspendGetUserInfo starting
Copy the code

When the coroutine runs, even though it’s still running in Main, it’s actually running in the next Main Looper.

suspend fun suspendGetUserInfo(userId: Long): UserInfo {
    // The switchable thread is the IO thread. The Main thread will be idle to perform other work
    log.debug("run in suspendGetUserInfo starting")
    GlobalScope.launch(Dispatchers.Main) {
        log.debug("Main is available")}val userInfo = withContext(Dispatchers.IO) {
        delay(100)
        log.debug("run in withContext")
        getUserFromNetwork(userId)
    }
    log.debug("run in suspendGetUserInfo finishing")
    return userInfo
}
Copy the code

What is the order of printing?

D: 12:03:06.469 main: Runin suspendGetUserInfo Starting D: 12:03:06.475 main: main is available D: 12:03:06.582 defaultDispatcher-worker-2: runinWithContext D: 12:03:06.585 main: runin suspendGetUserInfo finishing
Copy the code

Since withContext is a suspended function and has been switched to IO, Main is idle and the next Looper will execute the code in launch.

Suspend grammar rules

  • The Kotlin coroutine specifies onesuspendThe caller of the method must besuspendThe way to do it is tolaunch()/async()/runBlocking()The coroutine call that starts.
  • Coroutines have a number of base libraries withsuspendFunction, when we want to use, we should be careful to follow the above rules. Otherwise, it’s not usedsuspendWhere the function is,I don’t have to add to my function, Android Studio will also provide code hints.

Other suspended functions

In addition to withContext(), there’s

  • Like the one used aboveawait()
  • delay()Hang, hang, hang, hang, hang, hang, hang, hangsleep()
  • withTimeout { }Indicates that block execution will be thrown if it takes longer than a certain timeTimeoutCancellationException
public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope. () -> T
): T 
Copy the code
public suspend fun delay(timeMillis: Long)
Copy the code

How do I change the Callback code to Coroutine

If we use the Callback API to handle asynchronous logic, how do we change the Callback API to Coroutine?

suspendCancellableCoroutine/suspendCoroutine

// The original Callback form method
fun saveToRepositoryCallback(callback: (Int) -> Unit) {... } GlobalScope.launch(Dispatchers.IO) {val returnValue = saveToRepository()
}

// Coroutine form
suspend fun saveToRepository(a): Int {
  return suspendCoroutine { continuation ->
    saveToRepositoryCallback {
      continuation.resume(it)
    }
  }
}
Copy the code

To set the returned data with a continuation of a Callback is to change the Callback to return directly.

In addition, if you want a Coroutine to throw an exception, you can use resumeWithException to throw the exception.

The second argument is the exception Callback that is returned when an error occurs
fun saveToRepositoryCallback(success: (Int) -> Unit, error: (Throwable) -> Unit) {... } GlobalScope.launch(Dispatchers.IO) {try {
  	val returnValue = saveToRepository()
  } catch (t: Throwable) {
    // do something on error}}// Calling this method might throw an exception
suspend fun saveToRepository(a): Int {
  return suspendCoroutine { continuation ->
    saveToRepositoryCallback({
      continuation.resume(it)
    }, {
      continuation.resumeWithException(it)
    })
  }
}
Copy the code

SuspendCancellableCoroutine features

A Job started with suspendCoroutine will not be stopped with cancel()

val job = GlobalScope.launch(Dispatchers.IO) {
    val returnValue = saveToRepository()
    LogUtil.debug("returnValue: $returnValue")}// Coroutine form
suspend fun saveToRepository(a): Int {
    return suspendCoroutine { continuation ->
        saveToRepositoryCallback {
            LogUtil.debug("callback: $it")
            continuation.resume(it)
        }
    }
}
// Trigger cancellation
job.cancel()

I: [main] callback: 0
I: [DefaultDispatcher-worker2 -] returnValue: 0

Copy the code

If you use suspendCancellableCoroutine, running the returnValue code will not be run

val job = GlobalScope.launch(Dispatchers.IO) {
    val returnValue = saveToRepository()
    LogUtil.debug("returnValue: $returnValue")
}

suspend fun saveToRepository(a): Int {
    return suspendCancellableCoroutine { continuation ->
        saveToRepositoryCallback {
            LogUtil.debug("callback: $it")
            continuation.resume(it)
        }
    }
}

I: [main] callback: 0
Copy the code

conclusion

For Kotlin Coroutine, it’s more like a cross-threading tool. Think of it as a library like AsyncTask, Exeutors, Handler, Rxjava, etc. Coroutines are recommended for the following situations:

  • You have multiple concurrent tasks going on at the same time, or you want to improve performance through concurrency
  • When you need to switch between the UI thread and the worker thread

thinking

  • Can coroutines avoid blocking altogether?
  • Is it lighter than threading?

The appendix

How to add dependencies to Kotlin Coroutine: Add dependencies to build.gradle.

buildscript {
    ext {
    	kotlin_version = '1.3.50'
    	coroutines_android_version = '1.3.2'}}dependencies {
    / / rely on kotlin
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    // This is the coroutine Android library and also relies on the Kotlin Coroutine library
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_android_version"
}
Copy the code

If you find this post useful, please like, comment, bookmark and share.