Kotlin 协程-暂停与取消

2023-07-14 10:49:17 浏览数 (1)

本次主要学习如何进行协程的取消操作以及超时后协程的处理。

取消 cancel()

我们在进行开发的过程中。往往会由于各种需求会需要控制后台协程的细粒度。比如,界面关闭了。那么在这个界面中启动的协程已经不需要再执行了。

我们就需要触发取消事件。关闭该协程事项,回收内存。

示例:

代码语言:javascript复制
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

suspend fun main(string: Array<String>) {
    val job = GlobalScope.launch {
        repeat(1000) {
            println("协程执行:$it")
            delay(500L) //中断500毫秒
        }
    }
    println("主线程:开始中断")
    delay(2000L) //主线程中断20000毫秒
    println("主线程:中断结束")
    job.cancel() // 取消该协程
    job.join()// 等待协程事项执行结束
    println("整个流程结束了。")
}
//输出
主线程:开始中断
协程执行:0
协程执行:1
协程执行:2
协程执行:3
主线程:中断结束
整个流程结束了。

我们可以主动获取到协程对象。然后调用cencel 进行取消协程

在这里还有一个优化写法,就是将cencel和join方法一起执行。

示例:

代码语言:javascript复制
//    job.cancel() // 取消该协程
//    job.join()// 等待协程事项执行结束
    job.cancelAndJoin()

效果是一样的。

所有Kotlinx.coroutines中挂起的函数,都是可以被取消的。

但是有些情况下,必须等待处理结束了才能取消。

协程正在执行计算任务的时候。并且没有检查取消状态。

示例:

代码语言:javascript复制
import kotlinx.coroutines.*

  fun main()= runBlocking {
    val startTime = System.currentTimeMillis()

    val job = launch {
        (Dispatchers.Default) {
            var nextPrintTime = startTime
            var i = 0
            while (i < 5) {
                if (System.currentTimeMillis() >= nextPrintTime) {
                    println("协程:我逻辑执行${i  }")
                    nextPrintTime  = 500L //时间增加500毫秒
                }
            }
        }
    }
    println("主线程:开始中断")
    delay(1300L) //主线程中断1300毫秒
    println("主线程:中断结束")
    job.cancelAndJoin()
    println("主线程结束了。")
}
//输出
主线程:开始中断
协程:我逻辑执行0
协程:我逻辑执行1
协程:我逻辑执行2
主线程:中断结束
协程:我逻辑执行3
协程:我逻辑执行4
主线程结束了。

上面的例子,我们调用了取消协程。

但是协程仍然打印了两个输出,才在最后结束。

那么,我们如果面临这种情况下,仍然需要在结束的时候关闭协程该如何处理?

强制取消-显式检查取消状态

我们有两种方法来使执行计算的代码可以被取消。

  • 定期调用挂起函数来检查是否取消。(yield函数)
  • 显式的检查取消状态。

但是我们下面主要介绍显式检查取消状态,实现协程的关闭。

还是上面的例子,我们更换while的判断条件,就可以实现取消了。

isActive

示例:

代码语言:javascript复制
package com.zinyan.general
import kotlinx.coroutines.*

fun main()= runBlocking {
    val startTime = System.currentTimeMillis()

    val job = launch {
        (Dispatchers.Default) {
            var nextPrintTime = startTime
            var i = 0
            while (isActive) {
                if (System.currentTimeMillis() >= nextPrintTime) {
                    println("协程:我逻辑执行${i  }")
                    nextPrintTime  = 500L //时间增加500毫秒
                }
            }
        }
    }
    println("主线程:开始中断")
    delay(1300L) //主线程中断1300毫秒
    println("主线程:中断结束")
    job.cancelAndJoin()
    println("主线程结束了。")
}
//输出
主线程:开始中断
协程:我逻辑执行0
协程:我逻辑执行1
协程:我逻辑执行2
主线程:中断结束
主线程结束了。

上面的代码。关键地方就在于:isActive 它是一个可以被使用在CoroutineScope中的扩展属性。

CancellationExceptiond

协程在被取消的时候,都会抛出一个CancellationExceptiond的信息。我们通常情况下不捕获该信息也没有关系。但是如果面临上面这种,在计算的时候想关闭,那么我们可以主动检查

示例:

代码语言:javascript复制
package com.zinyan.general


import kotlinx.coroutines.*

fun main()= runBlocking {
    val startTime = System.currentTimeMillis()

    val job = launch {
        try{
            repeat(1000){
                println("协程:执行$it")
                delay(500L) //中断500毫秒
            }
        }finally {
            println("协程:触发了try事件")
        }
    }
    println("主线程:开始中断")
    delay(1300L) //主线程中断1300毫秒
    println("主线程:中断结束")
    job.cancelAndJoin()
    println("主线程结束了。")
}
//输出
主线程:开始中断
协程:执行0
协程:执行1
协程:执行2
主线程:中断结束
协程:触发了try事件
主线程结束了。

whithContext(NonCancellable)

我们如果觉得挂起取消的协程。直接finally里面结束不优雅,我们还可以使用withContext(NonCancellable)进行进一步处理。

示例:

代码语言:javascript复制
package com.zinyan.general


import kotlinx.coroutines.*

fun main()= runBlocking {
    val startTime = System.currentTimeMillis()

    val job = launch {
        try{
            repeat(1000){
                println("协程:执行$it")
                delay(500L) //中断500毫秒
            }
        }finally {
            withContext(NonCancellable){
                println("协程:触发了try事件")
                delay(1000L) //中断1000毫秒
                println("协程,我主动中断了1秒,因为我不应该直接取消")
            }
        }
    }
    println("主线程:开始中断")
    delay(1300L) //主线程中断1300毫秒
    println("主线程:中断结束")
    job.cancelAndJoin()
    println("主线程结束了。")
}

主要是想告诉我们协程是可以在运行的时候进行取消。但是相关方法我们在创建定义协程的时候,需要考虑如果协程被取消该如何处理数据。

超时 withtimeOut()

在开发中,绝大多数取消一个协程的理由是它有可能超时了。

那么针对超时,我们有一个单独的函数来处理

示例:

代码语言:javascript复制
package com.zinyan.general
import kotlinx.coroutines.*

fun main() = runBlocking {
    println("主线程:开始")
    withTimeout(1300L) {
        repeat(1000) {
            println("协程:执行事项$it")
            delay(500L)
        }
    }
}
//输出
主线程:开始
协程:执行事项0
协程:执行事项1
协程:执行事项2
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
	at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:186)
	at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:156)
	at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:497)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
	at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:69)
	at java.lang.Thread.run(Thread.java:748)

Process finished with exit code 1

执行后它会直接报出Timed out 错误。

TimeoutCancellationException 异常是 CancellationException 的子类。

由于Kotlin 将CancellationException的异常当做了正常的协程执行结束原因。所以我们在上面使用的时候,没有出现崩溃异常。而直接使用TimeOut 就会出现崩溃异常了。

我们该如何正确的使用呢?我们可以给方法添加try事件捕获,也可以是有它的其他方法,例如下面的。

示例:如果超时了输出null

代码语言:javascript复制
package com.zinyan.general
import kotlinx.coroutines.*

fun main() = runBlocking {
    println("主线程:开始")
    val ss = withTimeoutOrNull(1300L) {
        repeat(1000) {
            println("协程:执行事项$it")
            delay(500L)
        }
        "结束"
    }
    println("输出:$ss")
}
//输出
主线程:开始
协程:执行事项0
协程:执行事项1
协程:执行事项2
输出:null

超时与异步

我们在超时的过程中,往往会有很多属性和方法是异步的。我们如果发生了超时同时希望异步数据能够得到释放等操作那么我们该如何处理呢?

示例:

代码语言:javascript复制
package com.zinyan.general
import kotlinx.coroutines.*
var acquired = 0
class Resource {
    init { acquired   } // acquired 累加 添加资源
    fun close() { acquired-- } // acquired 减少 释放资源
}

fun main() {
    runBlocking {
        repeat(100_000) { // 创建10K的协程线层
            launch {
                val resource = withTimeout(60) { // 设置操时60毫秒
                    delay(50) // 中断50毫秒
                    Resource() // Resource访问对象
                }
                resource.close() // Resource访问对象 结束对象
            }
        }
    }
    println(acquired) // 协程已经全部允许完毕了 。我们打印资源对象
}
//输出
0 或者1 

上面的例子,我们如果只运行一遍我们可能大部分都将得到0这个值。

但你如果多允许几遍,可能就会有可能输出1。

如果调整一下超时时间和中断时间。你的电脑性能也将会影响这个参数的输出结果。

但是有没有办法,让这个输出稳定呢?当然有方法了。

示例:

代码语言:javascript复制
package com.zinyan.general

import kotlinx.coroutines.*

var acquired = 0

class Zinyan {
    init {
        acquired  
    } // acquired 累加 添加资源

    fun close() {
        acquired--
    } // acquired 减少 释放资源
}

fun main() {
    runBlocking {
        repeat(100_000) { // 创建10K的协程线层
            launch {
                var resource: Zinyan? = null
                try {
                    withTimeout(60) {
                        delay(50)
                        resource = Zinyan()
                    }
                } finally {
                    resource?.close()
                }
            }
        }
    }
    println(acquired) // 协程已经全部允许完毕了 。我们打印资源对象
}
//输出
0

上面这个例子永远都会输出0.并且不会造成资源泄露。

0 人点赞