Encapsulation and cancellation of coroutines in Kotlin

1. Encapsulate the callback interface in java into a suspend function

  • Take OKHttp request as an example, the code is as follows:
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine

suspend fun Call.await(): String = suspendCoroutine { block ->
    enqueue(object : Callback {
        override fun onFailure(call: Call, e: IOException) {
            block.resumeWithException(e)
        }

        override fun onResponse(call: Call, response: Response) {
            if (response.isSuccessful) {
                block.resume(response.body()!!.string())
            }
        }
    })
}

  • The use of the suspend function is as follows:
val client: OkHttpClient = OkHttpClient.Builder() .build()
        val request: Request = Request.Builder()
            .get()
            .url("http://xxx")
            .build()
  GlobalScope.async(Dispatchers.Main) {
            val call = client.newCall(request)
            val data = call.await()
        }

Because Call's extension function await() is a suspend function, it must run in a coroutine or other suspend function

  • Continue to extend OkHttp's Call class and add an await() function. The return value of the function is String type. Use the suspend function suspendCoroutine{} or suspendCancellableCoroutine{} in the function. When the await() function is called, it will first suspend the current The coroutine, and then execute enqueue to put the network request into the queue. When the request is successful, use block.resume(response.body()!!.string()) to resume the previous coroutine.

  • The function of suspendCancellableCoroutine{} function:

delay() function

Yield() function: Suspend the current coroutine, and then distribute the coroutine to the Dispatcher queue, so that the thread or thread pool where the coroutine is located can run other coroutine logic, and then continue to execute the original coroutine when the Dispatcher is idle . To put it simply, it is to give up its execution rights to other coroutines. When other coroutines complete or give up execution rights, the original coroutine can resume operation.

2. Father and Son Coroutine

  • The GlobalScope.launch() function and GlobalScope.async() function are called global coroutines without a parent coroutine.

  • When a coroutine is started in CoroutineScope by other coroutines, it will inherit the context through CoroutineScope.coroutineContext, and the job of this new coroutine will become a child task of the parent coroutine task. When a parent coroutine is cancelled, all its child coroutines will also be cancelled recursively. Examples are as follows:

GlobalScope.launch(Dispatchers.Main) {
            //父协程
            launch { 
                //子协程
            }
            async {
                //子协程 
            }
            withContext(coroutineContext){
                //子协程
            }
        }
  • The parent-child relationship between coroutines has three effects:

    • If the parent coroutine manually calls cancel() or ends abnormally, all its child coroutines will be cancelled immediately.

    • The parent coroutine must wait for all the child coroutines to complete (in the completed or canceled state) to complete.

    • When a child coroutine throws an uncaught exception, its parent coroutine is cancelled by default.

3. Coroutine cancellation

  • Example 1:
fun main() = runBlocking {
    val job = launch {
        launch {
            repeat(6) {
                println("run_child:$it")
                delay(500)
            }
        }
        repeat(6) {
            println("run_parent:$it")
            delay(500)
        }
    }
    delay(1600)
    job.cancel()
    println("job end")

}
  • result

  • After the job is cancelled in the above code, delay() will detect whether the coroutine has been cancelled, so after the job.cancel() call, the parent coroutine and child coroutine above will also be cancelled

  • Example 2:

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        var nextTime = 0L
        var i = 1
        while (i <= 3) {
            val nowTime = System.currentTimeMillis()
            if (nowTime >= nextTime) {
                println("parent_${i++}")
                nextTime = nowTime + 500L
            }
        }
    }
    delay(800)
    job.cancel()
    println("job end")
}
  • result

  • After the job is cancelled in the above code, there is no logic to detect the state of the coroutine, it is all calculation logic, so the calculation logic of the job will continue to run.

  • In order to cancel the operation logic of the coroutine in time, the state of the coroutine can be detected, and isActive can be used to judge. In the above example, you can replace while(i <= 3) with while(isActive). code show as below:

fun main() = runBlocking {
    val job = launch(Dispatchers.Default) {
        var nextTime = 0L
        var i = 1
        while (isActive) {
            val nowTime = System.currentTimeMillis()
            if (nowTime >= nextTime) {
                println("parent_${i++}")
                nextTime = nowTime + 500L
            }
        }
    }

    delay(1200)
    job.cancel()
    println("job end")

}
  • result

  • Run code blocks that cannot be canceled. After manually canceling the coroutine, a cancelable suspension function like delay() will throw a CancellationException when detecting the cancelled state, and then exit the coroutine. At this time, you can use try {…} finally {…} expressions or <T: Closeable?, R> T.use {} function to perform final actions or close resources. But if you call a custom or system cancelable suspend function in the finally block, a CancellationException will be thrown again. Usually we close a file in the finally block, cancel a task or close a communication channel are non-blocking, and will not call any suspend function. When you need to suspend a canceled coroutine, you can wrap the code in withContext(NonCancellable) {… }.

4. Timeout cancellation

  • The withTimeout() {…} suspension function has been provided in the coroutine library to automatically cancel the coroutine after the timeout. It will throw TimeoutCancellationException after timeout. It is a subclass of CancellationException. It is the normal reason for the end of the coroutine. Stack trace information will not be printed. If you need to perform some resource shutdown operations after cancellation, you can use the try mentioned earlier. {…} Finally {…} expression.
try {
    withTimeout(1300L) {
        repeat(1000) { i ->
                println("I'm sleeping $i ...")
            delay(500L)
        }
    }
} finally {
    println("I'm running finally")
}
  • There is also a withTimeoutOrNull() {…} suspend function that returns null after timeout instead of throwing an exception.
val result = withTimeoutOrNull(1300L) {
    repeat(1000) { i ->
            println("I'm sleeping $i ...")
        delay(500L)
    }
    "Done" /* 在它运行得到结果之前取消它,如果循环次数x延迟时间小等于1300则返回Done,否则返回null*/
}
println("Result is $result")

Guess you like

Origin blog.csdn.net/genmenu/article/details/87274409