- 作者:老汪软件技巧
- 发表时间:2024-11-08 07:01
- 浏览量:
在 kotlin 中,我们不可避免地会使用到协程,这篇文章将介绍一些协程中需要注意的要点。
协程的 cancel 不能中断线程
如果我们在协程中使用了线程的并发集合,比如LinkedBlockingQueue,ArrayBlockingQueue等等,这时候我们调用了它的阻塞接口,此时我们无法使用协程的 cancel来中断当前线程。这是因为在 Java 多线程中,我们需要使用interrupt方法才能中断线程,而 cancel 是不行的。kotlin 官方考虑到了这个问题,它提供了方便的 runInterruptible 方法。代码示例如下:
class Test {
suspend fun test() {
runInterruptible { // 内部会调用 interrupt 方法
while (true){
Thread.sleep(100)
println("do something")
}
}
}
}
fun main(): Unit = runBlocking {
val t = Test()
val job = launch(Dispatchers.IO) {
t.test()
}
delay(500)
job.cancel() // 这里只需要 cancel 就可以了
delay(1000)
println("end")
}
Job 的 cancel 才会取消协程
如下代码所示,我们创建一个 CoroutineContext 来启动一个协程,然后等待一段时间后 cancel 协程。执行后,我们可以发现 cancel 是没有效果的。
val threadContext = newSingleThreadContext("newThread")
CoroutineScope(threadContext).launch {
while (true) {
println("do something")
delay(500)
}
}
handler.postDelayed({
threadContext.cancel() // 没有效果
println("threadContext cancel")
}, 5000)
这是因为虽然我们创建的 CoroutineContext 有 cancel 方法,但是其内部实现是通过获取 CoroutineContext 的 Job 来 cancel 的。由于我们没有添加 Job,导致获取不到对应的 Job 对象,因此 cancel 没有执行。
/**
* Cancels [Job] of this context with an optional cancellation cause.
* See [Job.cancel] for details.
*/
public fun CoroutineContext.cancel(cause: CancellationException? = null) {
this[Job]?.cancel(cause)
}
如果你对 CoroutineContext 还不熟悉,可以看看 这篇文章。在文章中我们介绍过,CoroutineContext 的功能类似一个 Map,内部包含多个元素;而每一个元素都是 CoroutineContext 的实现,结构如下图所示:
创建新 Job 时注意不要破坏协程的结构化
当我们同时使用 CoroutineScope 和设置新的 CoroutineContext(包含Job) 时,哪个才是有效的?答案是后者,代码示例如下:
val newContext = threadContext + Job()
lifecycleScope.launch(newContext) {
while (true) {
println("do something")
delay(500)
}
}
handle.postDelayed({
lifecycleScope.cancel() // 无效
println("lifecycleScope cancel")
}, 1000)
handler.postDelayed({
newContext.cancel() // 有效
println("threadContext cancel")
}, 5000)
这是因为我们设置新的 Job 破坏了之前的结构。之前的结构是 lifecycleScope 的 Job 为 parent;而现在 新创建的 Job 为 parent。因此在设置新的 CoroutineContext 时需要注意,是否会影响到之前协程的结构。
挂起函数应该考虑协程取消
和 Java 的 interrupt 方法类似,kotlin 中的挂起函数默认是不会主动处理协程取消的。这需要你主动调用 ensureActive 方法,或者判断 isActive。
suspend fun testCancel() {
while (true) {
coroutineContext.ensureActive()
println("do something")
}
}
suspend fun testCancel() {
while (coroutineContext.isActive) {
println("do something")
}
}
协程中try-catch异常时考虑 CancellationExceptions
当我们调用 cancel 方法来取消协程时,其内部是通过抛出 CancellationExceptions 的异常来实现的。如下代码所示,如果我们使用 runCatching 或者 try-catch(e: Exception) 包裹协程方法时,如果此时取消协程,不是直接退出,而是继续执行。
lifecycleScope.launch(Dispatchers.IO) {
runCatching {
// do something
while (true) {
delay(500)
println("do something1")
}
}.onFailure {
println(it)
}
while (true) { // 执行这里,无法退出
println("do something2")
}
}
不要在子协程中设置 CoroutineExceptionHandler
使用 CoroutineExceptionHandler 处理复杂结构的协程异常时,它仅在顶层协程中起作用。
val handle = CoroutineExceptionHandler { coroutineContext, throwable ->
println("处理异常")
}
GlobalScope.launch(Dispatchers.IO) {
launch {
delay(200)
println("1")
}
launch(handle) { // 无效
delay(100)
throw NullPointerException()
}
}
正确代码示例如下:
val handle = CoroutineExceptionHandler { coroutineContext, throwable ->
println("处理异常")
}
GlobalScope.launch(Dispatchers.IO + handle) { // 放在顶层协程中才能起作用
launch {
delay(200)
println("1")
}
launch() {
delay(100)
throw NullPointerException()
}
}
使用 repeatOnLifecycle 而不是 launchWhenXXX
在 Android 中,推荐使用 repeatOnLifecycle 而不是 launchWhenXXX 来处理不同不同生命周期的事件。
lifecycleScope.launchWhenCreated {
// do something
}
lifecycleScope.launch(Dispatchers.IO) {
lifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
// do something
}
}
至于为什么使用 repeatOnLifecycle 而不是 launchWhenXXX,可以看看Jetpack MVVM七宗罪 之二:在 launchWhenX 中启动协程Jetpack MVVM 使用常见错误 这一篇文章。
参考