Kotlin语言协程是什么,有了解么?
- 协程是一种编程思想,并不局限于特定的语言,所以协程是一种语言特性。
- kotlinx.coroutines是由JetBrains开发的kotlin协程库。
- 协程最重要的是通过非阻塞挂起和恢复实现了异步代码的同步编写方式,把原本运行在不同线程的代码写在一个代码块{}里,看起来就像是同步代码。
协程到底有什么用?
主要的作用就是简化复杂的异步逻辑,以同步的写法达到异步的效果,让复杂的异步逻辑变得简单,方便开发者。
kotlin协程比起线程更加轻量?性能更好吗?
并不。
kotlin协程只是一种语言特性,底层还是得依靠执行它的机器,而线程属于操作系统层的一个抽象,所以本质没有变化,并不轻量,也不会提升性能。
如果硬要说,那也只能说是吞吐量比起直接使用线程更大了(也即是协程把线程给榨干了)。
如何简单的开启协程(不使用框架层的内容)
suspend {
println("我是协程,我被开启了")
}.startCoroutine(Continuation(EmptyCoroutineContext) {})
说说startCoroutine干了什么
获取coroutineContext的ContinuationInterceptor并进行拦截,然后在当前线程resume协程。
startCoroutine即在当前线程开启协程。
createCoroutine干了什么
对协程进行拦截。然后把拦截后的协程上下文放入SafeContinuation。
说说什么是SafeContinuation?有什么用途
一个安全的Continention。只能被resume一次,避免重复resume。
通常情况下为了确保挂起函数的执行流程的安全性,只要只要涉及到暴露continuation的场景通常都会使用safeContinuation包装。
因为就正常的程序执行流程来说,resume理应只被调用一次,invokeSuspend可以被调用多次。
所以SafeContinuation通过代理确保了开发者在使用时只会resume一次。
suspendCoroutine作用?用途?原理?
- 作用
立即挂起当前协程,并将resume的时机交由dev决定。
-
用途
比如Java回调转挂起函数。
-
原理
底层通过使用一个方法调用获取当前的Continuation,并包裹上一层SafeContinuation,最会将SafeContinuation通过高阶函数往外传,最后调用getOrThrow来判断是否脱离。如果block返回后还没有resume就会导致getOrThrow返回suspend。最后就是一层层返回。
suspendCancellableCoroutine作用?用途?原理?
和suspendCoroutine是类似的。只是多了一个取消的回调。
kotlin协程是如何挂起来的,又是如何恢复的
kotlin对于所有的挂起函数做了一些特殊的处理,修改了函数参数,函数返回值,以及函数体。
函数参数强行添加了一个Continuation,函数返回值改为了Object,函数体转化为了一个状态机,每一个case分支就对于一个挂起函数调用,通过continuation内的一个label参数决定执行那个挂起函数。
挂起本质就是直接return返回值,每个挂起函数调用都会有返回值,可能是函数本身的返回值,也可能是一个挂起的标志,也就是一个枚举类COROUTINE_SUSPENDED(CoroutineSingleton)
恢复就是在挂起函数挂起的位置重新执行,本质就是通过一个名为invokeSuspend的函数调用函数本身,由于挂起函数内部的状态机对挂起位置进行了保存,所有能在指定的位置恢复。
suspend main的实现原理?
- jvm默认是不支持挂起函数的
- suspend main只是在main函数的基础上进行了一层包装,创建了一个Continuation(RunSuspend)传入了suspend lambda里面。
sequence原理
- sequence利用了挂起函数的特性在一个线程里面循环的resume
- 通过sequence.iterator可以获取一个Continuation/Iterator
- 调用iterator的hasNext就会resume Sequence内的suspend lambda
- 而这个suspend lambda只会通过yield设置一个或者yieldAll设置一组值,然后yield以后就会挂起
- 在调用next的时候就可以拿到yield设置的值
这样通过hasNext和next组合调用就可以达到懒加载list的效果。即用即生成。
另外sequence是一个受限的Suspend函数。它的作用域被一个名为RestrictsSuspension的注解标记,也就是说只能调用作用域内部的挂起函数,作用域外的挂起函数是不能调用的(因为sequence本质上只是利用了挂起函数中断继续的特点,没有被挂起来做耗时任务)
说说CoroutineContext
CoroutineContext是一种介于Map和List的数据结构
说它是Map呢是因为CoroutinContext中的一个元素由两部分组成,一为Key,二为Element
说它是List呢是因为它在索引的时候并没有使用Map惯有的hash算法进行元素索引。而是通过逐一遍历的方式。
CoroutineContext不能装太多的元素,因为就设计上就没想过装很多的东西,只是存放挂起函数执行过程中的必要内容。
说说结构化并发
-
结构化并发的建立
结构化并发的建立依靠Job,结构化并发的建立就是生成一个节点树
这个过程发生在launch阶段在AbstractCoroutine init代码块attachChild实现
-
结构化取消
当调用cancel的时候最后会调用到JobSupport的
notifyCancelling,而notifyCancelling会先取消自己然后把异常向上通报给parent
parent这里对异常做了分流,如果是CancellationException就不继续向上传递并且不会取消自个儿,如果是其他异常会递归的向上取消
对于协程的取消有没有可能会出现无法取消的情况?
有!因为协程取消是需要条件的——必须在挂起的时候才可以被取消。
协程的取消需要依靠挂起后恢复的检查,如果调用一个挂起函数,而且这个函数一直运行没有被挂起,那么直接取消会失效。
通常的对于不会被挂起的场景,解决方案两种。
-
让他被挂起来(加个delay啥的。)(不是很推荐。不优雅)
val job = scope.launch { while (true) { delay(1) Thread.sleep(1000) println("I am alive") } } -
确保job是active。
val job = scope.launch { while (true) { ensureActive() Thread.sleep(1000) println("I am alive") } }val job = scope.launch { while (true) { yield() Thread.sleep(1000) println("I am alive") } }val job = scope.launch { while (true && isActive) { Thread.sleep(1000) println("I am alive") } }
协程取消底层是什么?为什么能将运行过程中的程序中断?
协程的取消的底层是采用的异常抛出。(其实协程的取消很大程度上和异常捕获有关系。)
首先job.cancel方法会调用到JobNode的invoke方法,接着就会调用到CancellableContinuationImpl的cancel方法。
CancellableContinuationImpl的cancel方法会将包装一层CancelledContinuation然后传入exception(CancellationException)最后resume。(这个过程和resumeWithException差别不大)。
public override fun cancel(cause: Throwable?): Boolean {
_state.loop { state ->
if (state !is NotCompleted) return false // false if already complete or cancelling
// Active -- update to final state
val update = CancelledContinuation(this, cause, handled = state is CancelHandler)
if (!_state.compareAndSet(state, update)) return@loop // retry on cas failure
// Invoke cancel handler if it was present
(state as? CancelHandler)?.let { callCancelHandler(it, cause) }
// Complete state update
detachChildIfNonResuable()
dispatchResume(resumeMode) // no need for additional cancellation checks
return true
}
}
接着就是这个CancellationException的异常处理了。
-
首先异常会在BaseContinuationImpl中的resumeWith方法中被catch到。
-
接着会调用completion.resumeWith将exception向Continuation的内层传递
-
不出意外会调用到AbstractCoroutine的resumeWith方法
-
然后再经过几层任务栈会调用到notifyCancelling
-
在notifyCancelling进行了如下操作
1.取消自己
2.取消自己的子job
3.取消父job
(这个过程是递归的)
private fun notifyCancelling(list: NodeList, cause: Throwable) { // first cancel our own children onCancelling(cause) notifyHandlers<JobCancellingNode>(list, cause) // then cancel parent cancelParent(cause) // tentative cancellation -- does not matter if there is no parent }
所以总的来说协程取消也就是:
-
先通过JobNode的invoke方法向Continuation里面注入一个CancellationException。
-
接着利用异常处理机制进行取消。
-
异常首先会达到AbstractCoroutine的resumeWith
-
经过一系列调度以后会先取消当前job
-
然后会把异常向上报给parentJob,parent会先对Exception进行判断
- 如果是CancellationException那就直接处理了,异常处理流程就完成了,或者说结束了
- 如果是其他的Exception,就会取消当前job(也就是父协程),至于异常是否上报这就看具体的job类型了。
-
-
接着通过resumeWith传入到AbstractCoroutine进行取消,经过一系列调度会调用到job的notifyCancelling。
-
1.取消自己
-
2.取消自己的子job
-
3.取消父job(这个过程是递归的)
对于CancellationException并非如此,CancellationException只会执行前两步,不会取消parent。
-
协程的异常传传播过程和抛出?(☞框架层)
kt协程内部有一套异常的传播规则。大体上是符合责任链设计的。
对于kt协程而言,它对于异常进行了分类:CancellationException和其他。
为什么要如此分类?
因为协程的取消也是依靠异常传播。
CancellationException和其他异常的处理有什么区别?
CancellationException会被协程内部处理消化,不会向外抛出,同时它也不会触发CoroutineExceptionHandler。
除CancellationException以外的所有异常不会被协程内部消化,会触发CoroutineExceptionHandler,而且对于没有被捕获的异常会通报给通过SPI机制引入的CoroutineExceptionHandler(也只会通报,SPI引入的handler无权做异常的捕获),并直接将异常抛出。
协程的异常传播。
-
起点?
当然是异常发生的位置,异常发生的位置呢,通常是协程体,而协程体在被编译以后会被Continuation的invokeSuspend函数里面调用。而调用invokeSuspend的呢一般呢都是resumeWith,其中BaseContinuationImpl的居多。
val outcome: Result<Any?> = try { val outcome = invokeSuspend(param) if (outcome === COROUTINE_SUSPENDED) return Result.success(outcome) } catch (exception: Throwable) { Result.failure(exception) } -
then?
resumeWith会将我们产生的异常catch掉,并resume给Continuation内组合的Continuaton(协程cps变换,Continuation Pass Style,调用挂起函数就是对Continuation进行包装,最后的挂起函数就是一个类似于洋葱的机构)
这行代码其实等价于resumeWithException。
completion.resumeWith(outcome)咋一看好像没啥,resumeWith,但这是异常处理的核心。
因为它接下来会把这个Exception交由框架层的抽象协程(AbstractCoroutine)处理。(基础层基本上不存在异常处理的,因为都是很单一的getOrThrow,getOrNull... )
-
AbstractCoroutine?
这是个什么东西
Abstract base class for implementation of coroutines in coroutine builders.
实现协程构建的基础类。
常见的launch,async,runblocking,coroutineScope,supervisorScope等都和它有些关系。
-
抽象协程是如何处理异常的?
AbstractCoroutine.resumeWith
好像还挺短...
public final override fun resumeWith(result: Result<T>) { val state = makeCompletingOnce(result.toState()) if (state === COMPLETING_WAITING_CHILDREN) return afterResume(state) }然而过程并不简单。
经过一长串的调用最后会通过notifyCancelling来取消协程。
private fun notifyCancelling(list: NodeList, cause: Throwable) { // first cancel our own children onCancelling(cause) notifyHandlers<JobCancellingNode>(list, cause) // then cancel parent cancelParent(cause) // tentative cancellation -- does not matter if there is no parent }在协程取消完以后通过调用finalizeFinishingState来决定异常被谁捕获
通常的只有当parentJob的cancelParrent返回false的时候才会调用调用job的handleException
// Now handle the final exception if (finalException != null) { val handled = cancelParent(finalException) || handleJobException(finalException) if (handled) (finalState as CompletedExceptionally).makeHandled() }
所以协程的异常处理是分两个过程的
-
协程取消
递归的JobNode树进行操作
1.取消自己
2取消子协程
3.取消parent(准确来说调用parentJob的childCancelled方法)
-
异常捕获
当取消完成以后会通过责任链的模式来判断谁处理这个异常
val handled = cancelParent(finalException)||handleJobException(finalException)先会询问parent是否处理,如果parent处理那么子类就没机会handleJobException,只要有一个没处理就给了子job处理异常的机会。(通常的是否处理由handlesException字段决定)
这段代码是说只要有parent(不管是几级parent)要处理这个异常,那么子job就得乖乖的把处理机会交上去。(而通常情况下顶层的CoroutineScope会自动加入一个JobImpl)
private fun handlesException(): Boolean { var parentJob = (parentHandle as? ChildHandleNode)?.job ?: return false while (true) { if (parentJob.handlesException) return true parentJob = (parentJob.parentHandle as? ChildHandleNode)?.job ?: return false } }
job层级关系?
suspend fun main() {
val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
launch {
while (true) {
delay(1000)
}
}
launch {
while (true) {
delay(1000)
}
}
}
//遍历
traverse(scope.coroutineContext.job,1)
}
fun traverse(job: Job, count: Int) {
println("第${count}层:$job")
val l = job.children.toList()
if (l.isEmpty()) return
for (e in l) {
traverse(e, count + 1)
}
}
输出结果这样的
第1层:JobImpl{Active}@26aa12dd
第2层:StandaloneCoroutine{Completing}@33c7e1bb
第3层:StandaloneCoroutine{Active}@34c4973
第3层:StandaloneCoroutine{Active}@17d677df
可以发现launch的过程就是在创建job树。
核心代码就在new StandaloneCoroutine的时候调用父类构造的时候。
即AbstractCoroutine的时候
init {
//默认为true
//注意会将coroutineContext的job作为parent来建立联系
if (initParentJob) initParentJob(parentContext[Job])
}
你的job用对了吗?
scope.launch {
launch(Job()) {
while (true) {
delay(1000)
}
}
launch {
while (true) {
delay(1000)
}
}
}
上述job的层级结构是什么样的?
第1层:JobImpl{Active}@182fb1e1 第2层:StandaloneCoroutine{Completing}@2cf94122 第3层:StandaloneCoroutine{Active}@3ab657c6
按理是4个?为什么只有3个?
CoroutineScope 1个
launch了一个
launch.launch了两个
1 + 1 + 2 = 4 ?
因为Job()的问题
job树的建立规则
init {
if (initParentJob) initParentJob(parentContext[Job])
}
Job()覆盖了CoroutineScope中的Job。也就是说launch(Job())这行代码使得launch的job与parent脱节了。
launch一个协程这个协程的parent是Job(),而不是原协程的parentJob。
而Job()没有传入parent那么默认就是null。
launch(Job())在job树的基础上又开了一个节点tree
解决方案?
val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
launch(Job(coroutineContext[Job])) {
while (true) {
delay(1000)
}
}
launch {
while (true) {
delay(1000)
}
}
}
//遍历
traverse(scope.coroutineContext.job,1)
第1层:JobImpl{Active}@26aa12dd
第2层:StandaloneCoroutine{Completing}@33c7e1bb
第3层:JobImpl{Active}@34c4973
第4层:StandaloneCoroutine{Active}@52feb982
第3层:StandaloneCoroutine{Active}@3043fe0e
至于为什么是job tree的深度为什么是5层?(前面有答案)
如何破除Job的父子关系?有何隐患?
suspend fun main() {
val scope = CoroutineScope(Dispatchers.Default)
val job = scope.launch {
launch (Job()){
while (true){
delay(1000)
}
}
launch {
while(true){
delay(1000)
}
}
}
traverse(scope.coroutineContext.job,1)
}
第1层:JobImpl{Active}@72b6cbcc
第2层:StandaloneCoroutine{Completing}@1f57539
第3层:StandaloneCoroutine{Active}@76f2b07d
不合理的job层级关系会导致任务无法被取消
suspend fun main() {
val scope = CoroutineScope(Dispatchers.Default)
val job = scope.launch {
launch (Job()){
while (true){
delay(1000)
println("A")
}
}
launch {
while (true){
delay(1000)
println("B")
}
}
}
//确保A B都正常运行起来才取消
delay(2000)
job.cancel()
//睡死
delay(Long.MAX_VALUE)
//traverse(scope.coroutineContext.job,1)
}
会发现A会一直打印。(为什么无法取消?因为A的parent是Job(),而Job()的parent是null)
这样开启无异于
suspend fun main() {
val scope = CoroutineScope(Dispatchers.Default)
val job = scope.launch {
GlobalScope.launch {
while (true){
delay(1000)
println("A")
}
}
launch {
while (true){
delay(1000)
println("B")
}
}
}
//确保A B都正常运行起来才取消
delay(2000)
job.cancel()
//睡死
delay(Long.MAX_VALUE)
//traverse(scope.coroutineContext.job,1)
}
对于需要打破父子job关系的场景,需要谨慎考虑。
superVisorJob为什么能阻止异常向上传播?为什么Job不行?
-
supervisorJob阻断异常的传播
suspend fun main() { val scope = CoroutineScope(Dispatchers.Default) val supervisor = scope.launch { launch(SupervisorJob(coroutineContext[Job])) { 1 / 0 } launch { while (true) { delay(1000) println("hello") } } } supervisor.join() }注:hello任务会一直打印,
-
job无法阻断异常的传播
suspend fun main() { val scope = CoroutineScope(Dispatchers.Default) val supervisor = scope.launch { launch(Job(coroutineContext[Job])) { 1 / 0 } launch { while (true) { delay(1000) println("hello") } } } supervisor.join() }注:在异常发生以后hello会停止打印
为什么会这样?
private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
override fun childCancelled(cause: Throwable): Boolean = false
}
因为SupervisorJob重写了childCancelled方法。
重新回到异常处理
private fun notifyCancelling(list: NodeList, cause: Throwable) {
// 取消自己和子协程
onCancelling(cause)
notifyHandlers<JobCancellingNode>(list, cause)
// 取消父协程
cancelParent(cause)
}
而cancelParent会调用谁?
private fun cancelParent(cause: Throwable): Boolean {
...
return parent.childCancelled(cause) || isCancellation
}
也就是说SupervisorJob对于子类出现的任何异常都是袖手旁观什么都不管,不会像Job(JobImpl)一样递归进行取消。
public open fun childCancelled(cause: Throwable): Boolean {
if (cause is CancellationException) return true
//当子job有异常抛到父job这里的时候会直接取消自个儿,这个cancelImpl还会触发类似的3段
//取消自己,取消子job,继续parent
return cancelImpl(cause) && handlesException
}
不使用SupervisorJob是否可以打破异常传播?
严格来说不行,但是放宽来说是可以的,通过打破父子job关系,可以对异常进行隔离,不过坏处是没法对任务进行结构化取消。
suspend fun main() {
val job = CoroutineScope(Dispatchers.Default).launch {
launch {
while (true) {
delay(1000)
println("hello")
}
}
launch(Job()) {
1 / 0
}
}
job.join()
}
虽然报异常了,但是不妨碍程序继续执行。
CoroutineExceptionHnader异常捕获的规则?
关于异常的处理过程很简答
val handled = cancelParent(finalException) || handleJobException(finalException)
先回问问parent,parent如果处理就没子job什么事情了,如果没处理才可。
下面看看不同的Job的处理情况
-
JobImpl
private fun handlesException(): Boolean { var parentJob = (parentHandle as? ChildHandleNode)?.job ?: return false while (true) { if (parentJob.handlesException) return true parentJob = (parentJob.parentHandle as? ChildHandleNode)?.job ?: return false } } -
StandaloneCoroutine
internal open val handlesException: Boolean get() = true -
SupervisorJob
不会上报。永远为false
可以发现:
- 直接launch的job会将异常传递给父类
- JobImpl是否上传异常这个和parent有关,只要有parent处理就上报,如果都不处理那么就不上报。
这样处理异常的job就可以确定下来了,也就是从异常发生点job开始,向上传递遇上第一个不处理异常的parent就停止传递。交给子job处理。
高端的bug通常只需要简单的处理方式
suspend fun main() {
val scope = CoroutineScope(Dispatchers.Default)
val job = scope.launch(CoroutineExceptionHandler { coroutineContext, throwable ->
println("catch Exception:$throwable")
}) {
launch {
throw Throwable("抛个异常不过分吧?")
}
}
job.join()
}
第一个不处理异常的Job是CoroutinsScope的JobImpl,所以会它的直接子job处理。
所以异常通过handleJobException交由处理
override fun handleJobException(exception: Throwable): Boolean {
handleCoroutineException(context, exception)
return true
}
处理不要太清晰
public fun handleCoroutineException(context: CoroutineContext, exception: Throwable) {
try {
//获取context内的CoroutineExceptionHandler然后处理
//如果设置有那就直接返回
context[CoroutineExceptionHandler]?.let {
it.handleException(context, exception)
return
}
} catch (t: Throwable) {
handleCoroutineExceptionImpl(context, handlerException(exception, t))
return
}
handleCoroutineExceptionImpl(context, exception)
}
如果context没有呢?
通知handlers(通过java spi机制引入的handler)
然后currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
(抛出。)
internal actual fun handleCoroutineExceptionImpl(context: CoroutineContext, exception: Throwable) {
// use additional extension handlers
for (handler in handlers) {
try {
handler.handleException(context, exception)
} catch (t: Throwable) {
// Use thread's handler if custom handler failed to handle exception
val currentThread = Thread.currentThread()
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, handlerException(exception, t))
}
}
// use thread's handler
val currentThread = Thread.currentThread()
// addSuppressed is never user-defined and cannot normally throw with the only exception being OOM
// we do ignore that just in case to definitely deliver the exception
runCatching { exception.addSuppressed(DiagnosticCoroutineContextException(context)) }
currentThread.uncaughtExceptionHandler.uncaughtException(currentThread, exception)
}
对于异常会尽量上传给父job,只要父job不处理那么锅就到了子job头上。
异常捕获规则
1
能捕获到异常?
try {
val job = CoroutineScope(Dispatchers.Default).launch {
1 / 0
}
job.join()
} catch (e: Exception) {
}
显然不能。launch相当于往线程池放runnable,异常抛出会在指定线程池,直接catch肯定拿不到。
2
CoroutineScope(Dispatchers.Default).launch {
launch {
launch(CoroutineExceptionHandler { context, throwable -> }) {
1 / 0
}
}
}.join()
显然不行。
CoroutineExceptionHandler必须要在父job不处理的情况下才会生效。
3
CoroutineScope(Dispatchers.Default).launch {
launch(Job()) {
launch(CoroutineExceptionHandler { context, throwable -> }) {
1 / 0
}
}
}.join()
显然不行,
Job()的直接子job是launch(Job())而不是launch(CoroutineExceptionHandler)所以CoroutineExceptionHandler是无权进行异常处理的。
4
CoroutineScope(Dispatchers.Default).launch {
launch(Job(coroutineContext[Job]) + CoroutineExceptionHandler { context, _ ->
println("catched")
}) {
launch() {
1 / 0
}
}
}
delay(Long.MAX_VALUE)
显然不行,Job没有打破父子job关系,所以先会问问父Job,如果处理就上传。
5
CoroutineScope(Dispatchers.Default).launch {
launch(SupervisorJob(coroutineContext[Job]) + CoroutineExceptionHandler { context, _ ->
println("catched")
}) {
launch() {
1 / 0
}
}
}
delay(Long.MAX_VALUE)
显然可以,SupervisorJob不会处理子job的任何异常。
6
coroutineScope {
launch(CoroutineExceptionHandler { context, throwable ->
println("catched")
}) {
1 / 0
}
launch {
delay(1000)
}
}
显然无法捕获到,为什么?
因为cancelParent对scopedCoroutine做了特殊处理。
if (isScopedCoroutine) return true
也就是说说一有异常coroutineScope就会外抛。
最后异常就会执行到continueCompleting.afterCompletion
resumeWithException
override fun afterCompletion(state: Any?) {
// Resume in a cancellable way by default when resuming from another context
uCont.intercepted().resumeCancellableWith(recoverResult(state, uCont))
}
7
try {
coroutineScope {
launch(CoroutineExceptionHandler { context, throwable ->
println("catched")
}) {
1 / 0
}
launch {
delay(1000)
}
}
}catch (e:Exception){
}
显然可以。详细见6
8
supervisorScope {
launch(CoroutineExceptionHandler { coroutineContext, throwable ->
println("catched")
}) {
1 / 0
}
launch {
}
}
显然可以
private class SupervisorCoroutine<in T>(
context: CoroutineContext,
uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
override fun childCancelled(cause: Throwable): Boolean = false
}
对于挂起函数的异常使用try catch进行捕获的时候需要注意什么?
val job = CoroutineScope(Dispatchers.Default).launch {
launch {
try {
delay(1000)
} catch (e: Exception) {
if (e is CancellationException) throw e
}
}
}
delay(100)
job.cancel()
将CancellationException抛出。因为任务的取消也是依靠的异常抛出,如果将CancellationException捕获了就会导致内部无法响应任务的取消。