1. 引言
前面已经知道了协程作用域和协程取消的真正作用了,现在结合着协程作用域和withContext
来再次体会下协程取消的便捷。
2. 实践代码说明
本文关键代码(按钮的点击事件):
viewBinding.launchBtn -> {
"Clicked launchBtn".let {
myLog(it)
}
scope.launch(Dispatchers.IO) {
"Coroutine IO runs (from launchBtn)".let {
myLog(it)
}
Thread.sleep(FIVE_SECONDS)
"Coroutine IO runs after thread sleep".let {
myLog(it)
}
withContext(Dispatchers.Main) {
"withContext(Dispatchers.Main) lambda".let {
myLog(it)
}
}
}
}
关键的代码逻辑很简单——
启动一个在IO线程的协程,协程输出第一行log——"Coroutine IO runs (from launchBtn)";
然后休眠线程5秒,输出第二行log——"Coroutine IO runs after thread sleep";
最后切换到主线程,输出第三行log——"withContext(Dispatchers.Main) lambda"。
这里所用的协程作用域跟前篇一样,是Activity中的属性:
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
生命周期onDestroy
中:
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
另外一个按钮,点击时取消已经启动的协程:
viewBinding.cancelBtn -> {
"Clicked cancelBtn".let {
myLog(it)
}
scope.coroutineContext.cancelChildren()
}
3. 实践过程说明
在启动协程后,5秒以内点击取消按钮或者退出当前页面,可以发现,协程的前两行log会始终输出,但是在第三行log却不会输出,不点击取消按钮和始终停留在当前页面的话,第三行log会正常输出。
为什么取消后第二行log始终输出,而第三行log不输出了呢??
在一学就会的协程使用——基础篇(三)初遇协程取消中,提及过协程的取消是需要协作的,也就是说,协程的取消需要在执行逻辑中需要有协作点!初遇篇所用的协程取消点主要是isActive
和ensureActive()
,这里初看并没有协程取消协作点,为什么第三行log不支持了呢?
这里便是本文的重点,所有kotlinx.coroutines包下的挂起函数都是可被取消的:
所有
kotlinx.coroutines
中的挂起函数都是 可被取消的 。它们检查协程的取消, 并在取消时抛出 CancellationException。
上述描述出自中文文档:https://www.kotlincn.net/docs/reference/coroutines/cancellation-and-timeouts.html
withContext
是kotlinx.coroutines包下的挂起函数,如上描述,是可被取消的。所以协程在执行到withContext
一行时,触发到协作点时,如果协程已经被取消,所以协作点生效。
这里便是解释了第三行log在取消后不再输出的问题。进一步地,为什么第二行log的执行时机也在协程取消以后,但第二行log始终会输出呢?这里必须再强调:
协程的取消是 协作 的。一段协程代码必须协作才能被取消。
也就是说,协程代码执行过程中,如果没有取消协作点,即使在协程执行到具体代码位置时协程已经被取消,协程仍会继续执行!
在第二行代码执行之时,没有任何协程取消协作点,所以不管执行第二行log输出之时协程是否已经被取消,第二行log始终会输出。
第三行log不输出,是因为挂起函数withContext
是可取消的,也就是在withContext
挂起函数执行的时候,才触发了协程取消的协作点,进而使得协程取消!
切记,协程取消不是万能钥匙,调用了协程的取消后,协程并不能在任意位置停止执行,只有执行到协作点的时候,协程的取消才会生效!
其实,想要在5秒内点击取消后第二行log也不输出,也很简单,在第二行log输出前,增加协程取消的协作点,即调用ensureActive()
函数即可!
4. 关于挂起函数的提醒
文档中描述,kotlinx.coroutines
中的挂起函数都是可被取消的。注意限定词,可被取消的不是挂起函数,是kotlinx.coroutines
中的挂起函数。
也就是说,挂起函数本身是不提供取消功能,只不过是kotlinx.coroutines
中的挂起函数中实现了对协程取消的协作代码。这里主要强调第一个容易误解的点:
挂起函数本身不会提供协程取消协作点,而是协程特定包下中的挂起函数内部实现代码提供了取消协作点。
事实上,上面说的可取消的挂起函数还限定了在kotlinx.coroutines
中,这句话简直就是完美且准确!
注意啊,这个包名的第一个是kotlinx,后面是有x的,Kotlin的包名中有一个跟这个很相似的,是kotlin.coroutines
,人家开发文档可没说kotlin.coroutines下面的挂起函数是可取消的!
比如suspendCoroutine
就是不带x的包名下的函数,所以这个挂起函数并不可取消。相对地,实现相同功能又可取消的函数为suspendCancellableCoroutine
,这个函数所在的包名是带x的。
这里suspendCoroutine
和suspendCancellableCoroutine
两个函数都是挂起函数,一个不可取消,一个可取消,另一方面也可以说明挂起函数并不总是支持取消协作的,取消的协作本质在挂起函数内部执行逻辑而与挂起函数无关。
这里,不妨再结合本文和一学就会的协程使用——基础篇(三)初遇协程取消中的代码,思考一下,协程的取消需要怎样的配合才能发挥取消的实际作用?
5. 样例工程代码
代码样例Demo,见Github:https://github.com/TeaCChen/CoroutineStudy
本文示例代码,如觉奇怪或啰嗦,其实为CancelStepTwoActivity.kt
中的代码摘取主要部分说明,在demo代码当中,为提升细节内容,有更加多的封装和输出内容。
本文的页面截图示例如下:
image-5-1.png一学就会的协程使用——基础篇(五)再遇协程取消(本文)
网友评论