## 协程取消
### 协程取消
在项目开发的过程中,当进入一个需要网络请求的界面中时,在该界面请求2秒,用户没有看到界面加载的数据就关闭了当前的界面,此时对应的网络请求任务就需要关闭掉,这个网络请求的线程也需要关闭。
同样的道理,在协程程序中,如果开启了一个协程来进行网络请求或者数据加载,当退出该界面时,该界面的数据还未加载完成,此时就需要取消协程。在Kotlin中是通过cancel()方法将协程取消的。接下来我们通过一个案例来演示如何取消协程,具体代码如下所示。
```
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.runBlocking
fun main(args: Array<String>): Unit = runBlocking {
val job = launch {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
delay(2000L)
println("协程取消前:isActive=${job.isActive} isCompleted=${job.isCompleted}")
job.cancel() //取消协程
println("协程取消后:isActive=${job.isActive} isCompleted=${job.isCompleted}")
}
```
运行结果:
```
I'm sleeping 0…
I'm sleeping 1…
I'm sleeping 2…
I'm sleeping 3…
协程取消前:isActive=true isCompleted=false
协程取消后:isActive=false isCompleted=true
```
上述代码中,第7行的repeat()方法表示的是重复1000次来打印“I'm sleeping$i…”,在第15行通过job.cancel()来取消协程,在取消协程的前后分别打印了job中任务的状态,根据该程序的运行结果可知,协程在取消之前isActive的值为true,isCompleted的值为false,表示该协程在活动中。由于在协程取消时,会出现两种情况,一种是正在取消,此时打印出的isActive的值为false,isCompleted的值为false;另一种是已经取消,此时打印出的isActive的值为false,isCompleted的值为true。这两种情况都表示协程取消成功。
>[info] **注意**
上述程序的运行结果中,协程取消后的信息有两种情况,具体如下。
第1种,正在取消协程时,运行结果为:
协程取消后:`isActive=false isCompleted=false
`
第2种,已经取消协程时,运行结果为:
协程取消后:`isActive=false isCompleted=true
`
#### **多学一招**:cancelAndJoin()函数与不可取消代码块
1. cancelAndJoin()函数与finally代码块
协程中的cancel()函数和join()函数是可以进行合并的,合并之后是一个cancelAndJoin()函数,这个函数用于取消协程。接下来我们通过一个案例来演示cancelAndJoin()函数取消协程以及在协程中使用try…finally代码块。具体代码如下所示。
```
import kotlinx.coroutines.experimental.cancelAndJoin
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.runBlocking
fun main(args: Array<String>): Unit = runBlocking {
val job = launch {
try {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
} finally {
println("之前最终执行的代码")
delay(1000L)
println("之后最终执行的代码")
}
}
delay(2000L)
job.cancelAndJoin()//取消协程
}
```
运行结果:
```
I'm sleeping 0…
I'm sleeping 1…
I'm sleeping 2…
I'm sleeping 3…
```
之前最终执行的代码
根据上述代码的运行结果可知,没有打印第16行代码需要打印的数据,这是由于当程序输出“I'm sleeping 3…”时,当前程序耗时是1500ms,主线程的延迟时间是2000ms,此时程序会继续执行finally中的代码。当执行完第14行代码时,程序需要延迟的时间为1000ms,此时主线程的延迟时间已经到了,主线程会继续运行第20行代码取消协程,由于协程结束时,守护线程也就结束,因此finally中的代码不会继续执行。
2. 不可取消代码块
如果想让【文件9-14】中的程序不受协程结束的影响,继续执行finally中的代码,则需要在finally中通过withContext{}代码块来实现,这个代码块称为不可取消的代码块,具体代码如下所示:
```
// 不可取消的代码块
withContext(NonCancellable){
println(" 之前最终执行的代码")
delay(1000L)
println(" 之后最终执行的代码")
}
```
### 协程取消失效
一般情况下,一个协程需要通过cancel()方法来取消,这种取消方式只适用于在协程代码中有挂起函数的程序。由于挂起函数在挂起时也就是等待时,该协程已经回到了线程池中,等待时间结束之后会重新从线程池中恢复出来,虽然可以通过cancel()方法取消这些挂起函数,但是在协程中调用某些循环输出数据的函数时,通过cancel()方法是取消不了这个协程的。接下来我们通过一个案例来演示通过cancel()方法无法取消的协程,具体代码如下所示。
~~~
import kotlinx.coroutines.experimental.*
fun main(args: Array<String>): Unit = runBlocking {
val job = launch(CommonPool) {
//程序运行时当前的时间
var nextTime = System.currentTimeMillis()
while (true) {
/* if(!isActive) return@launch //返回当前协程*/
try {
yield()
}catch (e:CancellationException){
println("异常名称=${e.message}")
return@launch
}
//每一次循环的时间
var currentTime = System.currentTimeMillis()
if (currentTime > nextTime) {
println("当前时间:${System.currentTimeMillis()}")
nextTime += 1000L
}
}
}
delay(2000L) //使程序延迟2秒钟
println("协程取消前:isActive=${job.isActive}")
job.cancel() //取消协程
job.join()
println("协程取消后:isActive=${job.isActive}")
}
~~~
运行结果:
```
当前时间:1531983528698
当前时间:1531983529698
当前时间:1531983530698
协程取消前:isActive=true
当前时间:1531983531698
当前时间:1531983532698
……
```
上述协程代码中,通过while循环每隔1000ms打印一次当前时间,如果通过cancel()方法来取消这个协程时,会发现该协程并没有停止,一直处于存活状态,并无限循环地打印数据,因此第23行代码中协程取消后的状态就不能打印了。如果协程的循环代码中没有挂起函数,则该程序是不能直接通过cancel()方法来取消的。
有一些协程中有循环代码且没有挂起函数的程序,如果想取消协程,则需要对这个协程中的Job任务状态进行判断。如果协程取消失效后,则可以通过以下两种方案来继续取消协程。
方案一:通过对isActive值的判断来取消协程}/pa
如果想要在结束协程时结束协程中的循环操作,则需要在循环代码中通过isActive的值来判断当前协程的状态,如果isActive的值为false,则表示当前协程处于结束状态,此时返回当前协程即可,具体代码如下所示:
```
//判断当前协程状态
if(!isActive) return@launch //返回当前协程
```
上述代码需要添加在【文件9-15】中的第11行代码上方。此时运行该文件中的程序,运行结果如下所示。
```
当前时间:1531984991424
当前时间:1531984992423
当前时间:1531984993423
协程取消前:isActive=true
协程取消后:isActive=false
```
方案二:使用yield()挂起函数来取消协程
除了上述解决方案之外,还可以在循环代码中调用yield()挂起函数来结束协程中的循环操作,因为调用cancel()函数来结束协程时,yield()会抛出一个异常,这个异常的名称是Cancellation Exception,抛出这个异常之后协程中的循环操作就结束了,同时在循环代码中通过try…catch来捕获这个异常并打印异常名称,当捕获到这个异常之后将协程返回即可。具体代码如下:
```
try {
yield()
}catch (e:CancellationException){
println(" 异常名称=${e.message}")
return@launch
}
```
上述代码需要添加在【文件9-15】中的第11行代码上方。此时运行该文件中的程序,运行结果如下所示。
```
当前时间:1531985095581
当前时间:1531985096581
当前时间:1531985097581
协程取消前:isActive=true
异常名称=Job was cancelled normally
协程取消后:isActive=false
```
### 定时取消
一般情况下,在挂起函数delay()中传递的时间到了之后会通过cancel()方法来取消协程,例如当打开一个应用的界面时,此时程序需要发送网络请求来获取界面中的数据,如果网络很慢、没有网络或者服务器有问题,请求3、4秒还没有请求到数据,则用户可能会没有耐心而将该界面关闭,此时后台请求的任务就断掉了,这样的用户体验很差。通常我们会给网络请求设置一个超时的时间,对于协程来说也是一样的,对于后台的耗时任务一般是需要设置一个时间的上限,时间到了之后就可以将这个协程取消。在协程中可以通过withTimeout()函数来限制取消协程的时间。接下来我们通过一个案例来演示如何通过withTimeout()函数在限制时间内取消协程,具体代码如【文件9-16】所示。
```
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.runBlocking
import kotlinx.coroutines.experimental.withTimeout
fun main(args: Array<String>): Unit = runBlocking {
val job = launch {
withTimeout(2000L) {
repeat(1000) { i ->
println("I'm sleeping $i ...")
delay(500L)
}
}
}
job.join()
}
```
运行结果:
```
I'm sleeping 0…
I'm sleeping 1…
I'm sleeping 2…
I'm sleeping 3…
```
上述代码中,通过withTimeout()函数来设置超过指定时间后协程会自动取消,withTimeout()函数中传递的2000L表示2秒。根据程序中的逻辑代码可知,每打印一行数据程序都会延迟500ms,打印4行数据后,程序的延迟时间一共为2000ms,等到下一次打印数据时,已经超过了协程的限制时间2秒,此时协程会自动取消,不再继续打印数据。
### 挂起函数的执行顺序
如果想要在协程中按照顺序执行程序中的代码,则只需要使用正常的顺序来调用即可,因为协程中的代码与常规的代码一样,默认情况下是按照顺序执行的。接下来我们通过执行两个挂起函数来演示协程中程序默认执行的顺序,具体代码如下所示。
```
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.runBlocking
import kotlin.system.measureTimeMillis
fun callMethod(): Unit = runBlocking {
val time = measureTimeMillis {
val a = doJob1()
val b = doJob2()
println("a=$a b=$b")
}
println("执行时间=$time")
}
suspend fun doJob1(): Int { //挂起函数doJob1()
println("do job1")
delay(1000L)
println("job1 done")
return 1
}
suspend fun doJob2(): Int { //挂起函数doJob2()
println("do job2")
delay(1000L)
println("job2 done")
return 2
}
fun main(args: Array<String>) {
callMethod()
}
```
运行结果:
```
do job1
job1 done
do job2
job2 done
a=1 b=2
执行时间=2051
```
上述代码中,创建了两个挂起函数,分别是doJob1()和doJob2(),运行每个函数时都通过delay()函数使程序延迟了1秒。在callMethod()方法中,通过measureTimeMillis()函数来获取程序运行两个挂起函数所耗费的时间。根据程序的运行结果可知,两个挂起函数的执行与其在程序中的调用顺序是一致的,运行两个挂起函数耗费的时间是2051,由此可以看出同步执行程序是比较耗时的。
### 通过async启动协程
上一小节的DoJob.kt文件中的代码是同步执行的,这样执行比较耗时。为了使程序执行不耗费很多时间,可以使用异步任务来执行程序。从概念上讲,异步任务就如同启动一个单独的协程,它是一个与其他所有协程同时工作的轻量级线程。前几节中的协程就属于一个异步任务,除了通过launch()函数来启动协程之外,还可以通过async()函数来启动协程,不同之处在于launch()函数返回的是一个Job任务并且不带任何结果值,而async()函数返回的是一个Deferred(也是Job任务,可以进行取消),这是一个轻量级的非阻塞线程,它有返回结果,可以使用.await()延期值来获取。接下来我们通过异步代码来执行上面的代码,修改后的代码如下所示。
```
import kotlinx.coroutines.experimental.async
import kotlinx.coroutines.experimental.runBlocking
import kotlin.system.measureTimeMillis
fun asyncCallMethod(): Unit = runBlocking {
val time = measureTimeMillis {
val a = async { doJob1() } //通过async函数启动协程
val b = async { doJob2() }
println("a=${a.await()} b=${b.await()}")
}
println("执行时间=$time")
}
fun main(args: Array<String>) {
asyncCallMethod()
}
```
运行结果:
```
do job1
do job2
job2 done
job1 done
a=1 b=2
执行时间=1045
```
上述代码中的函数doJob1()与doJob2()是前面示例中创建的,在此处不重复写一遍了,在程序中通过await()函数分别获取函数doJob1()与doJob2()的返回值。根据程序的运行结果可知,通过async()函数异步启动协程,程序的运行顺序不是默认的顺序,是随机的,并且根据程序的执行时间与前面示例中程序的执行时间对比可知,异步运行协程比同步运行要节省较多时间。
一般情况下,通过launch()函数启动没有返回值的协程,通过async()函数启动有返回值的协程。
### 协程上下文和调度器
在Kotlin中,协程的上下文使用CoroutineContext表示,协程上下文是由一组不同的元素组成,其中主要元素是前面学到的协程的Job与本小节要学习的调度器。协程上下文中包括协程调度程序(又称协程调度器),协程调度器可以将协程执行限制在一个特定的线程中,也可以给它分派一个线程池或者可以不做任何限制无约束地运行。所有协程调度器都接收可选的CoroutineContext参数,该参数可用于为新协程和其他上下文元素显示指定调度器。接下来我们通过一个案例来演示协程的上下文和调度器,具体代码如下所示。
```
import kotlinx.coroutines.experimental.*
fun main(args: Array<String>): Unit = runBlocking {
val list = ArrayList<Job>()
list += launch(Unconfined) { //主协程的上下文
println("Unconfined执行的线程=${Thread.currentThread().name}")
}
list += launch(coroutineContext) { //使用的是父协程的上下文
println("coroutineContext执行的线程=${Thread.currentThread().name}")
}
list += launch(CommonPool) { //线程池中的线程
println("CommonPool执行的线程=${Thread.currentThread().name}")
}
list += launch(newSingleThreadContext("new thread")) { //运行在新线程中
println("新线程执行的线程=${Thread.currentThread().name}")
}
list.forEach{
it.join()
}
}
```
运行结果:
```
Unconfined执行的线程=main
coroutineContext执行的线程=main
新线程执行的线程=new thread
CommonPool执行的线程=ForkJoinPool.commonPool-worker-1
```
根据该程序的运行结果可知,启动协程时,launch()函数中传递Unconfined主协程上下文时,程序执行的是主线程,传递coroutineContext父协程上下文时,程序执行的也是主线程,传递CommonPool线程池时,程序执行的是某一个线程,传递newSingleThreadContext("new thread")新线程时,程序执行的是新线程new thread。
>[info] 注意
上述程序中,由于执行的是4个协程,而协程是一种轻量级线程,多线程的执行顺序是不固定的,因此上述程序执行的先后顺序是不固定的。
### 父子协程
当使用coroutineContext(协程上下文)来启动另一个协程时,新协程的Job就变成父协程工作的一个子任务,当父协程被取消时,它的所有子协程也被递归地取消。接下来我们通过一个案例来演示取消父协程时,与其对应的子协程也会被取消,具体代码如下所示。
```
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import kotlinx.coroutines.experimental.runBlocking
fun main(args: Array<String>) :Unit= runBlocking{
val request = launch {
//父协程
val job1 = launch {
println("启动协程1")
delay(1000L)
println("协程1执行完成")
}
//子协程,使用的上下文是request对应的协程上下文
val job2 = launch (coroutineContext){
println("启动协程2")
delay(1000L)
println("协程2执行完成")
}
}
delay(500L)
request.cancel()
delay(2000L)
}
```
运行结果:
```
启动协程1
启动协程2
协程1执行完成
```
上述代码中,首先通过launch()函数启动了一个request协程,接着通过coroutineContext协程上下文启动了一个子协程job2,主线程中通过delay()函数一共延迟了2500ms,而开启的两个协程通过delay()函数一共延迟了2000ms,根据程序的运行结果可知,当通过cancel()方法取消主协程request时,子协程job2也自动取消了,因此运行结果没有打印“协程2执行完成”。
>[info] 注意
由于第20行代码中的cancel()方法的返回值是boolean类型,而main()函数不需要返回值,因此在这行代码下方任意输出一段字符串即可,不然程序会报错。
- 前言
- Kotlin简介
- IntelliJ IDEA技巧总结
- idea设置类注释和方法注释模板
- 像Android Studion一样创建工程
- Gradle
- Gradle入门
- Gradle进阶
- 使用Gradle创建一个Kotlin工程
- 环境搭建
- Androidstudio平台搭建
- Eclipse的Kotlin环境配置
- 使用IntelliJ IDEA
- Kotlin学习路线
- Kotlin官方中文版文档教程
- 概述
- kotlin用于服务器端开发
- kotlin用于Android开发
- kotlin用于JavaScript开发
- kotlin用于原生开发
- Kotlin 用于数据科学
- 协程
- 多平台
- 新特性
- 1.1的新特性
- 1.2的新特性
- 1.3的新特性
- 开始
- 基本语法
- 习惯用法
- 编码规范
- 基础
- 基本类型
- 包与导入
- 控制流
- 返回与跳转
- 类与对象
- 类与继承
- 属性与字段
- 接口
- 可见性修饰符
- 扩展
- 数据类
- 密封类
- 泛型
- 嵌套类
- 枚举类
- 对象
- 类型别名
- 内嵌类
- 委托
- 委托属性
- 函数与Lambda表达式
- 函数
- Lambda表达式
- 内联函数
- 集合
- 集合概述
- 构造集合
- 迭代器
- 区间与数列
- 序列
- 操作概述
- 转换
- 过滤
- 加减操作符
- 分组
- 取集合的一部分
- 取单个元素
- 排序
- 聚合操作
- 集合写操作
- List相关操作
- Set相关操作
- Map相关操作
- 多平台程序设计
- 平台相关声明
- 以Gradle创建
- 更多语言结构
- 解构声明
- 类型检测与转换
- This表达式
- 相等性
- 操作符重载
- 空安全
- 异常
- 注解
- 反射
- 作用域函数
- 类型安全的构造器
- Opt-in Requirements
- 核心库
- 标准库
- kotlin.test
- 参考
- 关键字与操作符
- 语法
- 编码风格约定
- Java互操作
- Kotlin中调用Java
- Java中调用Kotlin
- JavaScript
- 动态类型
- kotlin中调用JavaScript
- JavaScript中调用kotlin
- JavaScript模块
- JavaScript反射
- JavaScript DCE
- 原生
- 并发
- 不可变性
- kotlin库
- 平台库
- 与C语言互操作
- 与Object-C及Swift互操作
- CocoaPods集成
- Gradle插件
- 调试
- FAQ
- 协程
- 协程指南
- 基础
- 取消与超时
- 组合挂起函数
- 协程上下文与调度器
- 异步流
- 通道
- 异常处理与监督
- 共享的可变状态与并发
- Select表达式(实验性)
- 工具
- 编写kotlin代码文档
- 使用Kapt
- 使用Gradle
- 使用Maven
- 使用Ant
- Kotlin与OSGI
- 编译器插件
- 编码规范
- 演进
- kotlin语言演进
- 不同组件的稳定性
- kotlin1.3的兼容性指南
- 常见问题
- FAQ
- 与Java比较
- 与Scala比较(官方已删除)
- Google开发者官网简介
- Kotlin and Android
- Get Started with Kotlin on Android
- Kotlin on Android FAQ
- Android KTX
- Resources to Learn Kotlin
- Kotlin样品
- Kotlin零基础到进阶
- 第一阶段兴趣入门
- kotlin简介和学习方法
- 数据类型和类型系统
- 入门
- 分类
- val和var
- 二进制基础
- 基础
- 基本语法
- 包
- 示例
- 编码规范
- 代码注释
- 异常
- 根类型“Any”
- Any? 可空类型
- 可空性的实现原理
- kotlin.Unit类型
- kotlin.Nothing类型
- 基本数据类型
- 数值类型
- 布尔类型
- 字符型
- 位运算符
- 变量和常量
- 语法和运算符
- 关键字
- 硬关键字
- 软关键字
- 修饰符关键字
- 特殊标识符
- 操作符和特殊符号
- 算术运算符
- 赋值运算符
- 比较运算符
- 逻辑运算符
- this关键字
- super关键字
- 操作符重载
- 一元操作符
- 二元操作符
- 字符串
- 字符串介绍和属性
- 字符串常见方法操作
- 字符串模板
- 数组
- 数组介绍创建及遍历
- 数组常见方法和属性
- 数组变化以及下标越界问题
- 原生数组类型
- 区间
- 正向区间
- 逆向区间
- 步长
- 类型检测与类型转换
- is、!is、as、as-运算符
- 空安全
- 可空类型变量
- 安全调用符
- 非空断言
- Elvis操作符
- 可空性深入
- 可空性和Java
- 函数
- 函数式编程概述
- OOP和FOP
- 函数式编程基本特性
- 组合与范畴
- 在Kotlin中使用函数式编程
- 函数入门
- 函数作用域
- 函数加强
- 命名参数
- 默认参数
- 可变参数
- 表达式函数体
- 顶层、嵌套、中缀函数
- 尾递归函数优化
- 函数重载
- 控制流
- if表达式
- when表达式
- for循环
- while循环
- 循环中的 Break 与 continue
- return返回
- 标签处返回
- 集合
- list集合
- list集合介绍和操作
- list常见方法和属性
- list集合变化和下标越界
- set集合
- set集合介绍和常见操作
- set集合常见方法和属性
- set集合变换和下标越界
- map集合
- map集合介绍和常见操作
- map集合常见方法和属性
- map集合变换
- 集合的函数式API
- map函数
- filter函数
- “ all ”“ any ”“ count ”和“ find ”:对集合应用判断式
- 别样的求和方式:sumBy、sum、fold、reduce
- 根据人的性别进行分组:groupBy
- 扁平化——处理嵌套集合:flatMap、flatten
- 惰性集合操作:序列
- 区间、数组、集合之间转换
- 面向对象
- 面向对象-封装
- 类的创建及属性方法访问
- 类属性和字段
- 构造器
- 嵌套类(内部类)
- 枚举类
- 枚举类遍历&枚举常量常用属性
- 数据类
- 密封类
- 印章类(密封类)
- 面向对象-继承
- 类的继承
- 面向对象-多态
- 抽象类
- 接口
- 接口和抽象类的区别
- 面向对象-深入
- 扩展
- 扩展:为别的类添加方法、属性
- Android中的扩展应用
- 优化Snackbar
- 用扩展函数封装Utils
- 解决烦人的findViewById
- 扩展不是万能的
- 调度方式对扩展函数的影响
- 被滥用的扩展函数
- 委托
- 委托类
- 委托属性
- Kotlin5大内置委托
- Kotlin-Object关键字
- 单例模式
- 匿名类对象
- 伴生对象
- 作用域函数
- let函数
- run函数
- with函数
- apply函数
- also函数
- 标准库函数
- takeIf 与 takeUnless
- 第二阶段重点深入
- Lambda编程
- Lambda成员引用高阶函数
- 高阶函数
- 内联函数
- 泛型
- 泛型的分类
- 泛型约束
- 子类和子类型
- 协变与逆变
- 泛型擦除与实化类型
- 泛型类型参数
- 泛型的背后:类型擦除
- Java为什么无法声明一个泛型数组
- 向后兼容的罪
- 类型擦除的矛盾
- 使用内联函数获取泛型
- 打破泛型不变
- 一个支持协变的List
- 一个支持逆变的Comparator
- 协变和逆变
- 第三阶段难点突破
- 注解和反射
- 声明并应用注解
- DSL
- 协程
- 协程简介
- 协程的基本操作
- 协程取消
- 管道
- 慕课霍丙乾协程笔记
- Kotlin与Java互操作
- 在Kotlin中调用Java
- 在Java中调用Kotlin
- Kotlin与Java中的操作对比
- 第四阶段专题练习
- 朱凯Kotlin知识点总结
- Kotlin 基础
- Kotlin 的变量、函数和类型
- Kotlin 里那些「不是那么写的」
- Kotlin 里那些「更方便的」
- Kotlin 进阶
- Kotlin 的泛型
- Kotlin 的高阶函数、匿名函数和 Lambda 表达式
- Kotlin协程
- 初识
- 进阶
- 深入
- Kotlin 扩展
- 会写「18.dp」只是个入门——Kotlin 的扩展函数和扩展属性(Extension Functions / Properties)
- Kotlin实战-开发Android