​ 通过前面的三篇文章,我们已经讨论了协程的创建。有的时候,我们在启动了一个协程之后,并不需要该协程执行完毕,这个时候我们可以取消该协程的执行。比如在Android开发中,我们打开了一个页面,我们在进入页面的时候启动了一个协程来发起了网络请求,但是用户立马就关闭了页面,这个时候我们就可以取消这个协程的执行,因为我们已经不需要它的执行结果了。

  1. 我们先来回顾一下CoroutineScope.launch{}的方法签名:

    public fun CoroutineScope.launch(context: CoroutineContext = EmptyCoroutineContext,start: CoroutineStart = CoroutineStart.DEFAULT,block: suspend CoroutineScope.() -> Unit): Job
    

    可以看到,它有一个Job类型的返回值,它有对应的cancel()方法来取消执行的协程:

    fun main() = runBlocking {val job = launch {repeat(200) {println("hello : $it")delay(500)}}delay(1100)println("world")job.cancel()job.join()println("welcome")
    }
    

    运行结果为:

    hello : 0
    hello : 1
    hello : 2
    world
    welcome

    在delay 1100毫秒之后,由于在runBlocking协程(姑且称之)中调用了job.cancel()之后,launch协程(姑且称之)中原本会repeat 200次的执行,如今只计数到了2,说明的的确确被取消了。cancel()一般会和join()方法一起使用,因为cancel可能不会立马取消对应的协程(下面我们会提到,协程能够被取消,是需要一定条件的),所以会需要join()来协调两个协程。故而有个更加简便的方法:Job.cancelAndJoin(),可以用来替换上面的两行代码。

    public suspend fun Job.cancelAndJoin() {cancel()return join()
    }
    
  2. 协程能够被取消的前提条件

    只有协程代码是可取消的,cancel()才能起作用。

    Coroutine cancellation is cooperative. A coroutine code has to cooperate to be cancellable.

    这是官方的描述。我们来直接看一段代码:

    fun main() = runBlocking {val job = launch(context = Dispatchers.Default) {println("Current Thread : ${Thread.currentThread()}")var nextActionTime = System.currentTimeMillis()var i = 0while (i < 20) {if (System.currentTimeMillis() >= nextActionTime) {println("Job: ${i++}")nextActionTime += 500}}}delay(1300)println("hello")job.cancelAndJoin()println("welcome")
    }
    

    这段代码我们要注意两点:

    • 调用launch方法时,我们给其形参context多传递了一个Dispatcheres.Default参数。在这里我只告诉大家,这样使得launch启动的协程代码运行在一个新的子线程中,而不是和runBlocking协程一样(它是运行在主线程中)。(下一篇我们再来详细阐述这个地方)

    • 理解一下launch协程中的循环计算代码:

      第一次循环:i=0,同时 if条件肯定满足,输出”Job:0“,nextActionTime += 500

      第二次循环:由于nextActionTime在第一次循环中加了500,而且if中两行代码的执行时间肯定远远 不足500毫秒

      第…次循环:…

      直到等足了500毫秒,才第二次进入if条件,使用i++,nextActionTime += 500

      最终当i=20时,循环条件不满足,退出循环,至此launch协程代码执行完毕。

      在空等500毫秒中,实际上可以看做是死循环了500毫秒,并且一直占用着cpu。

    我们来看运行结果:

    按照我们本来的认知,在delay 1300毫秒之后,由于我们调用了cancelAndJoin方法,应该会取消launch子协程的运行才对(换句话说i最大值为2,而不会加到20才退出)。也就是说,取消没有成功。现在,我们再回过头来,理解”只有协程代码是可取消的,cancel()才能起作用“。那也就是说,这个示例中的launch协程的代码是不可取消的。那么什么样的代码才可以视为可取消的呢

    • kotlinx.coroutines包下的所有挂起函数都是可取消的。这些挂起函数会检查协程的取消状态,当取消时就会抛出CancellationException异常
    • 如果协程正在处于某个计算过程当中,并且没有检查取消状态,那么它就是无法被取消的

    很显然,我们上面示例中的代码就是计算过程中,所以它是无法被取消的。那么有没有什么方式使得这样的计算代码也变为可取消的呢?

    • 可以周期性地调用一个挂起函数,因为该挂起函数会取检查取消状态。
    • 显式地去检查取消状态

    下面我们就对刚刚的代码做一下改进:

    fun main() = runBlocking {val job = launch(Dispatchers.Default) {println("Current Thread : ${Thread.currentThread()}")var nextActionTime = System.currentTimeMillis()var i = 0while (isActive) {if (System.currentTimeMillis() >= nextActionTime) {println("Job: ${i++}")nextActionTime += 500}}}delay(1300)println("hello")job.cancelAndJoin()println("welcome")
    }
    

    输出结果:

    Current Thread : Thread[DefaultDispatcher-worker-1,5,main]
    Job: 0
    Job: 1
    Job: 2
    hello
    welcome

    这样我们就能成功的取消了计算过程中的协程。

    最后,我们对协程取消条件做一下总结:从某种角度上讲,是否能够取消是主动的;外部调用了cancel方法后,相当于是发起了一条取消信号;被取消协程内部如果自身检测到自身状态的变化,比如isActive的判断以及所有的kotlinx.coroutines包下挂起函数,都会检测协程自身的状态变化,如果检测到通知被取消,就会抛出一个CancellationException的异常。

  3. 下面看一波这样的示例代码:

    fun main() = runBlocking {val job = launch {try {repeat(200) {println("job: I am sleeping $it")delay(500)}}catch (e: CancellationException){println("canceled")}finally {println("finally块")}}delay(1300)println("hello")job.cancelAndJoin()println("welcome")}
    

    job: I am sleeping 0
    job: I am sleeping 1
    job: I am sleeping 2
    hello
    canceled
    finally块
    welcome

    这块可以说明两个问题:

    • 就是前面提到的CancellationException
    • 我们可以在finally代码块中对于一些资源的关闭和回收
  4. 现在有一个问题:对于大多数资源的关闭和回收(比如关闭文件、取消job等),都是瞬间的动作,都不会是阻塞的行为。可能在极少数情况下,关闭和回收的操作是阻塞的,是需要调用挂起函数的,但是在finally中,如果协程已经被取消,那么此时对于挂起函数的调用,都会抛出一个CancellationException的异常。那么这种情况下,我们又该如何去处理:

    fun main() = runBlocking {val job = launch {try {repeat(200) {println("job: I am sleeping $it")delay(500)}} finally {withContext(NonCancellable){println("finally块")delay(1000)println("after delay in finally block.")}}}delay(1300)println("hello")job.cancelAndJoin()println("welcome")}
    
    /*
    Calls the specified suspending block with a given coroutine context, suspends until it completes, and returns the result.
    */
    public suspend fun <T> withContext(context: CoroutineContext,block: suspend CoroutineScope.() -> T
    ): T
    
    public object NonCancellable : AbstractCoroutineContextElement(Job), Job {.....
    }
    
    • withContext: 在给定的协程上下文下调用指定的挂起代码块,会一直挂起,直到结果返回,后面在介绍协程在Android开发的应用时,会时常看到它的身影。

    • NonCancellable:它是一个object对象,并且它是不会被取消的,它的状态一直是active的。

      A non-cancelable job that is always [active][Job.isActive]. It is designed for [withContext] function
      * to prevent cancellation of code blocks that need to be executed without cancellation.
      
  5. CancellationException。既然当协程处于取消状态时,对于挂起函数的调用,会导致该异常的抛出,那么我们为什么没有在输出终端见到它的身影呢?因为kotlin的协程是这样规定的:

    That is because inside a cancelled coroutine CancellationException is considered to be a normal reason for coroutine completion.

    也就是说,CancellationException这个异常是被视为正常现象的取消。

  6. 父子协程的取消。

    前面我们已经讨论了协程的取消自身的种种,那么如果父协程取消,对子协程有什么影响呢?同样地,子协程的取消,会对父协程有什么影响呢?

    /* Jobs can be arranged into parent-child hierarchies where cancellation
    * of a parent leads to immediate cancellation of all its [children]. Failure or cancellation of a child
    * with an exception other than [CancellationException] immediately cancels its parent. This way, a parent
    * can [cancel] its own children (including all their children recursively) without cancelling itself.
    *
    */
    

    这一段是Job这个接口的文档注释,我截取了一部分出来。我们一起来看下这段文档说明:

    Job可以被组织在父子层次结构下,当父协程被取消后,会导致它的子协程立即被取消。一个子协程失败或取消的异常(除了CancellationException),它也会立即导致父协程的取消。

    下面我们就通过代码来证明这一点:

    a. 父协程取消对于子协程的影响:

    fun main() = runBlocking {val parentJob = launch {launch {println("child Job: before delay")delay(2000)println("child Job: after delay")}println("parent Job: before delay")delay(1000)println("parent Job: after delay")}delay(500)println("hello")}
    

    这是没调用cancel的代码,输出结果如下:

    parent Job: before delay
    child Job: before delay
    hello
    parent Job: after delay
    child Job: after delay

    做一下变动:

    fun main() = runBlocking {val parentJob = launch {launch {println("child Job: before delay")delay(2000)println("child Job: after delay")}println("parent Job: before delay")delay(1000)println("parent Job: after delay")}delay(500)parentJob.cancelAndJoin()println("hello")}
    

    我们在delay(500)之后添加一行:parentJob.cancelAndJoin(),再看输出结果:

    parent Job: before delay
    child Job: before delay
    hello

    可以看到,我们一旦取消父协程对应的Job之后,子协程的执行也被取消了,那么也就验证父协程的取消对于子协程的影响。

    b. 子协程正常的CancellationException取消:

    fun main() = runBlocking {val parentJob = launch {val childJob = launch {println("child Job: before delay")delay(2000)println("child Job: after delay")}println("parent Job: before delay")delay(1000)childJob.cancelAndJoin()println("parent Job: after delay")}delay(500)println("hello")}
    

    输出结果为:

    parent Job: before delay
    child Job: before delay
    hello
    parent Job: after delay

    可以看到,如果子协程是正常的取消(即CancellationException),那么对于父协程是没有影响的。

    c. 子协程的非CancellationException取消

    fun main() = runBlocking {val parentJob = launch {val childJob = launch {println("child Job: before delay")delay(800)throw RuntimeException("cause to cancel child job")}println("parent Job: before delay")delay(1000)childJob.cancelAndJoin()println("parent Job: after delay")}delay(500)println("hello")}
    

    输出结果:

    parent Job: before delay
    child Job: before delay
    hello
    Exception in thread “main” java.lang.RuntimeException: cause to cancel child job

    这样非CancellationException导致的子协程地取消,也会导致父协程的取消。

  7. 提问:A协程有两个子协程B、C,如果B由于非CancellationException导致被取消,那么C会受到影响吗?

    这个也不难得出答案,B的非CancellationException导致的取消,自然会导致父协程A被取消,那么C作为A的子协程也会被取消。

  8. 说明:以上的讨论是返回Job的协程且不考虑SupervisorJob的存在,后面还会学习到返回Deferred的协程以及SupervisorJob(它和我们在Android开发中使用协程息息相关)。

  9. 协程的超时取消。

    如果用于执行某个任务的协程,我们设定,如果它超过某个时间后,还未完成,那么我们就需要取消该协程。我们可以使用withTimeout轻松实现这一功能:

    fun main() = runBlocking {val result =  withTimeout(1900) {repeat(3) {println("hello: $it")delay(400)}"hello world"}println(result)
    }
    

    这种情况下没有超时,输出结果为:

    hello: 0
    hello: 1
    hello: 2

    “hello world”

    我们修改一下超时时间为1100,这时的输出结果为:

    hello: 0
    hello: 1
    hello: 2
    Exception in thread “main” kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1100 ms

    这样就把超时转换成了普通的异常,我们可以对异常进行捕获:

    fun main() = runBlocking {try {val result =   withTimeout(1100) {repeat(3) {println("hello: $it")delay(400)}"hello world"}println(result)} catch (e: TimeoutCancellationException) {println("超时取消")}
    }
    

    hello: 0
    hello: 1
    hello: 2
    超时取消

    与之类似地还有withTimeoutOrNull:

    fun main() = runBlocking {val result = withTimeoutOrNull(1900) {repeat(3) {println("hello: $it")delay(400)}"hello world"}println("the result is : $result")
    }
    

    输出结果为:

    hello: 0
    hello: 1
    hello: 2
    the result is : hello world

    再次修改超时时间:

    fun main() = runBlocking {val result = withTimeoutOrNull(1100) {repeat(3) {println("hello: $it")delay(400)}"hello world"}println("the result is : $result")
    }
    

    运行结果如下:

    hello: 0
    hello: 1
    hello: 2
    the result is : null

    可以看到,withTimeoutOrNull与withTimeout的区别在于,当发生超时取消后,withTimeoutOrNull的返回为null,而withTimeout会抛出一个TimeoutCancellationException。

    public suspend fun <T> withTimeoutOrNull(timeMillis: Long, block: suspend CoroutineScope.() -> T): T? {if (timeMillis <= 0L) return nullvar coroutine: TimeoutCoroutine<T?, T?>? = nulltry {return suspendCoroutineUninterceptedOrReturn { uCont ->val timeoutCoroutine = TimeoutCoroutine(timeMillis, uCont)coroutine = timeoutCoroutinesetupTimeout<T?, T?>(timeoutCoroutine, block)}} catch (e: TimeoutCancellationException) {// Return null if it's our exception, otherwise propagate it upstream (e.g. in case of nested withTimeouts)if (e.coroutine === coroutine) {return null}throw e}
    }
    

    之所以有这样的区别,我们可以从withTimeoutOrNul的源码中得出答案:它对TimeoutCancellationException进行了捕获。

Kotlin学习系列之:协程的取消和超时相关推荐

  1. Kotlin学习笔记24 协程part4 协程的取消与超时

    参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 1 如何取消协程 import kotlinx.coroutines.*/*** 协程的取消*/fun main() = ...

  2. Kotlin学习笔记26 协程part6 协程与线程的关系 Dispatchers.Unconfined 协程调试 协程上下文切换 Job详解 父子协程的关系

    参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 1 协程与线程的关系 import kotlinx.coroutines.* import java.util.concu ...

  3. Kotlin学习笔记25 协程part5 协程的同步与异步

    参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 1 程序运行时间统计measureTimeMillis /*** 程序运行时间统计measureTimeMillis** ...

  4. Kotlin学习笔记22 协程part2 join CoroutineScope 协程vs线程

    参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 1 Job的join方法 import kotlinx.coroutines.* /*** Job的join方法* 它会挂 ...

  5. Kotlin学习笔记21 协程part1 基本概念

    参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 本节先介绍协程的相关概念 概念可能枯燥,我们先要了解协程中的相关概念 然后结合代码理解这些概念 加深印象 协程的定义 协程 ...

  6. Kotlin学习——简单运用协程网络下载图片并更新到UI

    kotlin学习 协程Coroutines学习 简单小Demo:通过协程下载一张网络图片并显示出来 文章目录 kotlin学习 前言 一.如何开启一个协程? 二.如何在项目中使用协程 增加对 Kotl ...

  7. Kotlin学习笔记23 协程part3 lambda表达式深入 挂起函数 全局协程

    参考链接 示例来自bilibili Kotlin语言深入解析 张龙老师的视频 1 lambda表达式深入 /*** lambda 表达式深入* 当函数参数是函数时 并且该函数只有一个参数 可以不传入任 ...

  8. 【Kotlin 协程】协程取消 ② ( CPU 密集型协程任务取消 | 使用 isActive 判定协程状态 | 使用 ensureActive 函数取消协程 | 使用 yield 函数取消协程 )

    文章目录 一.CPU 密集型协程任务取消 二.使用 isActive 判定当前 CPU 密集型协程任务是否取消 三.使用 ensureActive 自动处理协程退出 四.使用 yield 函数检查协程 ...

  9. Kotlin开发利器之协程

    Kotlin开发利器之协程 协程的定义   协程的开发人员 Roman Elizarov 是这样描述协程的:协程就像非常轻量级的线程.线程是由系统调度的,线程切换或线程阻塞的开销都比较大.而协程依赖于 ...

  10. Kotlin的协程,延时、超时(7秒后超时,并中断执行的任务)

    Kotlin协程 简介: 优点:写法很简单,轻量级,挂起几乎不消耗内存,速度上优于java的线程,性能损耗小,能大幅度提高并发性能,本人推荐使用协程,而不用传统的线程 GlobalScope是生命周期 ...

最新文章

  1. php操作mysql数据库的扩展有哪些_8.PHP操作MySQL数据库(Mysqli扩展)
  2. 006_表的CRUD的操作
  3. java swing图形界面开发 java.swing简介
  4. XenServer XAPI简介
  5. 贷款太多怎么缓解压力?
  6. iBATIS In Action:iBATIS的安装和配置
  7. 【Keras中文文档】Layer Convolutional网址
  8. ShadowGun 的学习笔记 - GodRays
  9. crmeb pro版获取短信验证码失败解决方法
  10. 少儿编程学习(顺序结构)
  11. 802.11协议总结
  12. 离开马云后,20位阿里人的区块链创业路
  13. 简述完整的计算机组成部分组成部分组成,简述计算机系统的组成
  14. 技术问答-5 String StringBuilder StringBuffer
  15. iwork09破解方法及解决SFCompatibility错误方法
  16. Unity3D 实现本地排行榜功能
  17. 免听选考c语言程序设计难,2020春C语言程序设计(江俊君)-中国大学mooc-题库零氪...
  18. 教育直播APP开发,在线教育系统开发(功能)
  19. 3、AspnetCore 在接口调用时不成功
  20. 慕尼黑工业大学开源含四季的数据集:用于自动驾驶的视觉长期定位

热门文章

  1. C++面向对象小练习:几何图形类
  2. com词根词缀_英语词根词缀,cor和con分别代表什么意思
  3. jacob+wps 文档转为pdf excel转换pdf卡死无响应
  4. 【原创】LabView制作实时读取Excel正态分布图
  5. 徐思201771010132 《面向对象程序设计(java)》课程学习总结
  6. python解二元一次方程组 迭代法_解二元一次方程组多种方法
  7. SSM5.2版本整合
  8. 如何制作语音聊天程序源码,制作语音社交交友APP
  9. 大学生论文发表的费用需要多少
  10. 设计师们必须要知道的素材网站