ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
## 协程取消 ### 协程取消 在项目开发的过程中,当进入一个需要网络请求的界面中时,在该界面请求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()函数不需要返回值,因此在这行代码下方任意输出一段字符串即可,不然程序会报错。