一学就会的协程使用——基础篇(五)再遇取消

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不输出了呢??

在一学就会的协程使用——基础篇(三)初遇协程取消中,提及过协程的取消是需要协作的,也就是说,协程的取消需要在执行逻辑中需要有协作点!初遇篇所用的协程取消点主要是isActiveensureActive(),这里初看并没有协程取消协作点,为什么第三行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的。

这里suspendCoroutinesuspendCancellableCoroutine两个函数都是挂起函数,一个不可取消,一个可取消,另一方面也可以说明挂起函数并不总是支持取消协作的,取消的协作本质在挂起函数内部执行逻辑而与挂起函数无关。

这里,不妨再结合本文和一学就会的协程使用——基础篇(三)初遇协程取消中的代码,思考一下,协程的取消需要怎样的配合才能发挥取消的实际作用?

5. 样例工程代码

代码样例Demo,见Github:https://github.com/TeaCChen/CoroutineStudy

本文示例代码,如觉奇怪或啰嗦,其实为CancelStepTwoActivity.kt中的代码摘取主要部分说明,在demo代码当中,为提升细节内容,有更加多的封装和输出内容。

本文的页面截图示例如下:

image-5-1.png

一学就会的协程使用——基础篇

一学就会的协程使用——基础篇(一)协程启动

一学就会的协程使用——基础篇(二)线程切换

一学就会的协程使用——基础篇(三)初遇协程取消

一学就会的协程使用——基础篇(四)协程作用域

一学就会的协程使用——基础篇(五)再遇协程取消(本文)

一学就会的协程使用——基础篇(六)初识挂起

一学就会的协程使用——基础篇(七)初识结构化

一学就会的协程使用——基础篇(八)初识协程异常

一学就会的协程使用——基础篇(九)异常与supervisor

你可能感兴趣的:(一学就会的协程使用——基础篇(五)再遇取消)