Cursor blinking

Kotlin 协程 Job

Android 基础|协程|字数 2,491|阅读时长≈ 7 分钟
Kotlin 协程 Job

Job 生命周期

Job 是协程的句柄。对于您创建的每个协程(通过启动 launch 或异步 async),它都会返回一个 Job (Deferred 也是一种类型的 Job)实例,该实例唯一标识该协程并管理其生命周期

Job 可以经历一组状态:新建、活动、完成、已完成、取消和已取消。虽然我们无法访问状态本身,但我们可以访问作业的属性:isActive、isCancelled 和 isCompleted。

0_zGHzocA6-lCxk0aX.webp
0_zGHzocA6-lCxk0aX.webp

如果协程处于活动状态,则协程失败或调用 job.cancel() 将使作业进入取消状态(isActive = false,isCancelled = true)。一旦所有子进程都完成了工作,协程将进入 Canceled 状态并且 isCompleted = true。

Job 操作

挂起协程直到该作业完成。当 job 因任何原因完成且调用 job 所在的父协程的 parentJob 仍处于活动状态时,此调用将正常恢复(除了发生异常)。如果 job 仍处于新状态,此函数还会启动相应的协程。请注意,只有当所有子作业都完成时,该作业才算完成。

Code
  @Test    fun test_coroutineJob1() {        runBlocking {            val parentCoroutineContext = Job() + Dispatchers.Default + CoroutineName("Parent")            val scope = CoroutineScope(parentCoroutineContext)            val jobA = scope.launch(Dispatchers.IO + CoroutineName("A")) {                delay(1000)                println("A task end  ")            }            jobA.join() // join 挂起函数,等 jobA 执行完成后才会恢复            val jobB = scope.launch() {                println("B task start")                delay(2000)                println("B task end")            }        }        Thread.sleep(5000)    } 
Code
======> [15:34:18.568][DefaultDispatcher-worker-1 @A#2](ExampleUnitTest_CoroutineJob.kt:67) A task end  ======> [15:34:18.573][DefaultDispatcher-worker-1 @Parent#3](ExampleUnitTest_CoroutineJob.kt:71) B task start======> [15:34:20.581][DefaultDispatcher-worker-1 @Parent#3](ExampleUnitTest_CoroutineJob.kt:73) B task end

函数用于取消当前协程的执行。这个函数接受一个可选的 cause 参数,用于指定取消的原因。如果不提供 cause 参数,那么取消操作将以默认的 CancellationException 作为原因。 当调用 cancel() 函数时,协程会收到一个取消请求,并且在适当的时机会被取消。取消操作并不是立即生效的,而是在协程的挂起点(如 delay()、yield() 等)或者明确检查取消状态的地方生效。

Code
@Test    fun test_coroutineCancel2() {        runBlocking {            val scope = CoroutineScope(Job() + Dispatchers.Default + CoroutineName("Parent"))            val jobA = scope.launch(Dispatchers.IO + CoroutineName("A")) {                try {                    delay(2000)                    println("A task end  ")                } catch (e: Exception) {                    println(e)                }            }            val jobB = scope.launch() {                println("B task start")                delay(2000)                println("B task end")            }            delay(1300)            jobA.cancel(CancellationException("主动取消"))        }        Thread.sleep(5000)    }
Code
======> [16:07:00.536][DefaultDispatcher-worker-3 @Parent#3](ExampleUnitTest_CoroutineJob.kt:119) B task start======> [16:07:01.850][DefaultDispatcher-worker-3 @A#2](ExampleUnitTest_CoroutineJob.kt:115) java.util.concurrent.CancellationException: 主动取消======> [16:07:02.541][DefaultDispatcher-worker-3 @Parent#3](ExampleUnitTest_CoroutineJob.kt:121) B task end

协程通过抛出一个特殊的异常来处理取消操作:CancellationException 。如果您想要提供更多关于取消原因的细节,可以在调用在调用cancel()方法时传入一个 CancellationException 实例,如果使用缺省调用,则会创建一个默认的CancellationException 实例。 因为协程的取消会抛出CancellationException ,所以我们可以利用这个机制来对协程的取消进行一些操作(比如释放资源)。 在底层,子协程的取消会通过抛出异常来通知父级,而父级根据取消的原因来确定是否需要处理异常。如果子协程是由于CancellationException 而被取消,那么父级就不需要再执行其他操作(父级协程不会终止自身)。

有两种方法可以等待协程的结果:从启动返回的作业可以调用 join,并且可以等待从异步返回的延迟(一种作业类型)。 Job.join 暂停协程,直到工作完成。与 job.cancel 一起使用,它的行为如您所期望的:

  • 如果您先调用 job.cancel,然后调用 job.join,协程将暂停,直到作业完成。
  • 在 job.join 之后调用 job.cancel 没有任何效果,因为作业已经完成。

当您对协程的结果感兴趣时,可以使用 Deferred。当协程完成时,Deferred.await 返回此结果。 Deferred是Job的一种,它也可以被取消。

Code
 @Test    fun test_coroutineCancel3() {        runBlocking {            val scope = CoroutineScope(Job() + Dispatchers.Default + CoroutineName("Parent"))            val deferredA = scope.async(Dispatchers.IO + CoroutineName("A")) {                try {                    delay(2000)                    println("A task end  ")                } catch (e: Exception) {                    println(e)                }                return@async "A"            }            val jobB = scope.launch() {                println("B task start")                delay(2000)                println("B task end")            }            delay(1300)            deferredA.cancel(CancellationException("主动取消"))        }        Thread.sleep(5000)    }
Code
======> [16:12:42.758][DefaultDispatcher-worker-3 @Parent#3](ExampleUnitTest_CoroutineJob.kt:146) B task start======> [16:12:44.066][DefaultDispatcher-worker-3 @A#2](ExampleUnitTest_CoroutineJob.kt:141) java.util.concurrent.CancellationException: 主动取消======> [16:12:44.765][DefaultDispatcher-worker-3 @Parent#3](ExampleUnitTest_CoroutineJob.kt:148) B task end

取消协程

取消正在进行的协程

当启动多个协程时,逐个跟踪或取消它们可能会很麻烦,但是我们可以依靠取消父协程或协程作用域,因为这将取消它创建的所有协程。

Code
runBlocking {            val scope = CoroutineScope(Job() + Dispatchers.Default + CoroutineName("Parent"))            val jobA = scope.launch(CoroutineName("A")) {                println("A task start")                delay(1000)                println("A task end")            }            val jobB = scope.launch {                println("B task start")                delay(2000)                println("B task end")            }            scope.cancel()        }
Code
======> [16:18:18.327][DefaultDispatcher-worker-2 @Parent#3](ExampleUnitTest_CoroutineJob.kt:169) B task start======> [16:18:18.327][DefaultDispatcher-worker-1 @A#2](ExampleUnitTest_CoroutineJob.kt:164) A task start

取消一个协程作用域将同时取消此作用域下的所有子协程

有时您可能只需要取消一个协程,调用jobA.cancel()可确保仅取消特定协程,所有它的同级协程都不会受到影响。

Code
runBlocking {            val scope = CoroutineScope(Job() + Dispatchers.Default + CoroutineName("Parent"))            val jobA = scope.launch(CoroutineName("A")) {                println("A task start")                delay(1000)                println("A task end")            }            val jobB = scope.launch {                println("B task start")                delay(2000)                println("B task end")            }            jobA.cancel()        }
Code
======> [16:19:56.030][DefaultDispatcher-worker-2 @Parent#3](ExampleUnitTest_CoroutineJob.kt:191) B task start======> [16:19:56.030][DefaultDispatcher-worker-1 @A#2](ExampleUnitTest_CoroutineJob.kt:186) A task start======> [16:19:58.040][DefaultDispatcher-worker-1 @Parent#3](ExampleUnitTest_CoroutineJob.kt:193) B task end

被取消的子协程不会影响到其他同级的协程

取消协程的执行

在一个长时间运行的应用程序中,你也许需要对你的后台协程进行细粒度的控制。 比如说,一个用户也许关闭了一个启动了协程的界面,那么现在协程的执行结果已经不再被需要了,这时,它应该是可以被取消的。 该 launch 函数返回了一个可以被用来取消运行中的协程的 Job

Code
 runBlocking {            // sampleStart            val job = launch {                repeat(1000) { i ->                    println("job: I'm sleeping $i ...")                    delay(500L)                }            }            println("main: I'm tired of waiting!")            job.cancel() // 取消该作业            job.join() // 等待作业执行结束            println("main: Now I can quit.")            // sampleEnd        }
Code
======> [Test worker @coroutine#2](ExampleUnitTest.kt:47) job: I'm sleeping 0 ...======> [Test worker @coroutine#2](ExampleUnitTest.kt:47) job: I'm sleeping 1 ...======> [Test worker @coroutine#2](ExampleUnitTest.kt:47) job: I'm sleeping 2 ...======> [Test worker @coroutine#1](ExampleUnitTest.kt:52) main: I'm tired of waiting!======> [Test worker @coroutine#1](ExampleUnitTest.kt:55) main: Now I can quit.

一旦 main 函数调用了 job.cancel,我们在其它的协程中就看不到任何输出,因为它被取消了。 这里也有一个可以使 Job 挂起的函数 cancelAndJoin 它合并了对 cancel 以及 join 的调用。

取消是协作的

协程的取消是 协作 的。一段协程代码必须协作才能被取消。 所有 kotlinx.coroutines 中的挂起函数都是 可被取消的 。它们检查协程的取消, 并在取消时抛出 CancellationException。 然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的,就如如下示例代码所示:

Code
 runBlocking {            val startTime = System.currentTimeMillis()            val job = launch(Dispatchers.Default) {                var nextPrintTime = startTime                var i = 0                while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU                    // 每秒打印消息两次                    if (System.currentTimeMillis() >= nextPrintTime) {                        println("job: I'm sleeping ${i++} ...")                        nextPrintTime += 500L                    }                }            }            delay(1300L) // 等待一段时间            println("main: I'm tired of waiting!")            job.cancelAndJoin() // 取消一个作业并且等待它结束            println("main: Now I can quit.")        }
Code
======> [21:38:13.611][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:38) job: I'm sleeping 0 ...======> [21:38:14.098][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:38) job: I'm sleeping 1 ...======> [21:38:14.598][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:38) job: I'm sleeping 2 ...======> [21:38:14.909][Test worker @coroutine#1](ExampleUnitTest.kt:44) main: I'm tired of waiting!======> [21:38:15.098][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:38) job: I'm sleeping 3 ...======> [21:38:15.598][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:38) job: I'm sleeping 4 ...======> [21:38:15.598][Test worker @coroutine#1](ExampleUnitTest.kt:46) main: Now I can quit. 

我们可以看到它连续打印出了“I'm sleeping”,甚至在调用取消后, 作业仍然执行了五次循环迭代并运行到了它结束为止。

通过捕获 CancellationException 并且不重新抛出它,可以观察到相同的问题:

Code
 runBlocking {            val startTime = System.currentTimeMillis()            val job = launch(Dispatchers.Default) {                repeat(5) { i ->                    try {                        // print a message twice a second                        println("job: I'm sleeping $i ...")                        delay(500)                    } catch (e: Exception) {                        // log the exception                        println(e)                    }                }            }            delay(1300L) // 等待一段时间            println("main: I'm tired of waiting!")            job.cancelAndJoin() // 取消一个作业并且等待它结束            println("main: Now I can quit.")        }
Code
======> [21:41:29.814][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:35) job: I'm sleeping 0 ...======> [21:41:30.324][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:35) job: I'm sleeping 1 ...======> [21:41:30.832][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:35) job: I'm sleeping 2 ...======> [21:41:31.111][Test worker @coroutine#1](ExampleUnitTest.kt:44) main: I'm tired of waiting!======> [21:41:31.122][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:39) kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@446d4f33======> [21:41:31.123][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:35) job: I'm sleeping 3 ...======> [21:41:31.123][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:39) kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@446d4f33======> [21:41:31.123][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:35) job: I'm sleeping 4 ...======> [21:41:31.123][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:39) kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@446d4f33======> [21:41:31.124][Test worker @coroutine#1](ExampleUnitTest.kt:46) main: Now I can quit.

使计算代码可取消

我们有两种方法来使执行计算的代码可以被取消。第一种方法是定期调用挂起函数来检查取消。对于这种目的 yield 是一个好的选择。 另一种方法是显式的检查取消状态。让我们试试第二种方法。

将前一个示例中的 while (i < 5) 替换为 while (isActive) 并重新运行它。

Code
 runBlocking {            val startTime = System.currentTimeMillis()            val job = launch(Dispatchers.Default) {                var nextPrintTime = startTime                var i = 0                while (isActive) { // 可以被取消的计算循环                    // 每秒打印消息两次                    if (System.currentTimeMillis() >= nextPrintTime) {                        println("job: I'm sleeping ${i++} ...")                        nextPrintTime += 500L                    }                }            }            delay(1300L) // 等待一段时间            println("main: I'm tired of waiting!")            job.cancelAndJoin() // 取消该作业并等待它结束            println("main: Now I can quit.")        }
Code
======> [21:45:17.819][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:38) job: I'm sleeping 0 ...======> [21:45:18.304][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:38) job: I'm sleeping 1 ...======> [21:45:18.804][DefaultDispatcher-worker-1 @coroutine#2](ExampleUnitTest.kt:38) job: I'm sleeping 2 ...======> [21:45:19.112][Test worker @coroutine#1](ExampleUnitTest.kt:44) main: I'm tired of waiting!======> [21:45:19.113][Test worker @coroutine#1](ExampleUnitTest.kt:46) main: Now I can quit.

现在循环被取消了。isActive 是一个可以被使用在 CoroutineScope 中的扩展属性

在 finally 中释放资源

我们通常使用如下的方法处理在被取消时抛出 CancellationException 的可被取消的挂起函数。比如说,try {……} finally {……} 表达式以及 Kotlin 的 use 函数一般在协程被取消的时候执行它们的终结动作:

Code
 runBlocking {            val job = launch {                try {                    repeat(1000) { i ->                        println("job: I'm sleeping $i ...")                        delay(500L)                    }                } finally {                    println("job: I'm running finally")                }            }            delay(1300L) // 延迟一段时间            println("main: I'm tired of waiting!")            job.cancelAndJoin() // 取消该作业并且等待它结束            println("main: Now I can quit.")        }

join 和 cancelAndJoin 等待了所有的终结动作执行完毕, 所以运行示例得到了下面的输出

Code
======> [21:46:36.967][Test worker @coroutine#2](ExampleUnitTest.kt:34) job: I'm sleeping 0 ...======> [21:46:37.475][Test worker @coroutine#2](ExampleUnitTest.kt:34) job: I'm sleeping 1 ...======> [21:46:37.983][Test worker @coroutine#2](ExampleUnitTest.kt:34) job: I'm sleeping 2 ...======> [21:46:38.263][Test worker @coroutine#1](ExampleUnitTest.kt:42) main: I'm tired of waiting!======> [21:46:38.288][Test worker @coroutine#2](ExampleUnitTest.kt:38) job: I'm running finally======> [21:46:38.289][Test worker @coroutine#1](ExampleUnitTest.kt:44) main: Now I can quit. 

运行不能取消的代码块

在前一个例子中任何尝试在 finally 块中调用挂起函数的行为都会抛出 CancellationException,因为这里持续运行的代码是可以被取消的。通常,这并不是一个问题,所有良好的关闭操作(关闭一个文件、取消一个作业、或是关闭任何一种通信通道)通常都是非阻塞的,并且不会调用任何挂起函数。然而,在真实的案例中,当你需要挂起一个被取消的协程,你可以将相应的代码包装在 withContext(NonCancellable) {……} 中,并使用 withContext 函数以及 NonCancellable 上下文,见如下示例所示:

Code
runBlocking {            val job = launch {                try {                    repeat(1000) { i ->                        println("job: I'm sleeping $i ...")                        delay(500L)                    }                } finally {                    withContext(NonCancellable) {                        println("job: I'm running finally")                        delay(1000L)                        println("job: And I've just delayed for 1 sec because I'm non-cancellable")                    }                }            }            delay(1300L) // 延迟一段时间            println("main: I'm tired of waiting!")            job.cancelAndJoin() // 取消该作业并等待它结束            println("main: Now I can quit.")        }
Code
======> [21:48:47.071][Test worker @coroutine#2](ExampleUnitTest.kt:36) job: I'm sleeping 0 ...======> [21:48:47.574][Test worker @coroutine#2](ExampleUnitTest.kt:36) job: I'm sleeping 1 ...======> [21:48:48.081][Test worker @coroutine#2](ExampleUnitTest.kt:36) job: I'm sleeping 2 ...======> [21:48:48.366][Test worker @coroutine#1](ExampleUnitTest.kt:48) main: I'm tired of waiting!======> [21:48:48.394][Test worker @coroutine#2](ExampleUnitTest.kt:41) job: I'm running finally======> [21:48:49.401][Test worker @coroutine#2](ExampleUnitTest.kt:43) job: And I've just delayed for 1 sec because I'm non-cancellable======> [21:48:49.404][Test worker @coroutine#1](ExampleUnitTest.kt:50) main: Now I can quit.

超时

在实践中绝大多数取消一个协程的理由是它有可能超时。 当你手动追踪一个相关 Job 的引用并启动了一个单独的协程在延迟后取消追踪,这里已经准备好使用 withTimeout 函数来做这件事。 来看看示例代码:

Code
 runBlocking {            withTimeout(1300L) {                repeat(1000) { i ->                    println("I'm sleeping $i ...")                    delay(500L)                }            }        }
Code
======> [21:51:06.172][Test worker @coroutine#1](ExampleUnitTest.kt:36) I'm sleeping 0 ...======> [21:51:06.685][Test worker @coroutine#1](ExampleUnitTest.kt:36) I'm sleeping 1 ...======> [21:51:07.189][Test worker @coroutine#1](ExampleUnitTest.kt:36) I'm sleeping 2 ... Timed out waiting for 1300 mskotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

withTimeout 抛出了 TimeoutCancellationException,它是 CancellationException 的子类。 我们之前没有在控制台上看到堆栈跟踪信息的打印。这是因为在被取消的协程中 CancellationException 被认为是协程执行结束的正常原因。 然而,在这个示例中我们在 main 函数中正确地使用了 withTimeout

由于取消只是一个例外,所有的资源都使用常用的方法来关闭。 如果你需要做一些各类使用超时的特别的额外操作,可以使用类似 withTimeout 的 withTimeoutOrNull 函数,并把这些会超时的代码包装在 try {...} catch (e: TimeoutCancellationException) {...} 代码块中,而 withTimeoutOrNull 通过返回 null 来进行超时操作,从而替代抛出一个异常:

Code
 runBlocking {            val result = withTimeoutOrNull(1300L) {                repeat(1000) { i ->                    println("I'm sleeping $i ...")                    delay(500L)                }                "Done" // 在它运行得到结果之前取消它            }            println("Result is $result")        }  
Code
======> [21:52:38.292][Test worker @coroutine#1](ExampleUnitTest.kt:37) I'm sleeping 0 ...======> [21:52:38.804][Test worker @coroutine#1](ExampleUnitTest.kt:37) I'm sleeping 1 ...======> [21:52:39.309][Test worker @coroutine#1](ExampleUnitTest.kt:37) I'm sleeping 2 ...======> [21:52:39.612][Test worker @coroutine#1](ExampleUnitTest.kt:42) Result is null

异步超时和资源

withTimeout 中的超时事件相对于其块中运行的代码是异步的,并且可能随时发生,甚至在从超时块内部返回之前。如果您在块内打开或获取某些需要在块外关闭或释放的资源,请记住这一点。

例如,这里我们用 Resource 类模仿可关闭资源,该类通过递增获取的计数器并在其关闭函数中递减计数器来简单地跟踪它被创建的次数。现在让我们创建很多协程,每个协程在withTimeout块的末尾创建一个Resource,并在块外释放资源。我们添加了一个小的延迟,以便在 withTimeout 块已经完成时更有可能发生超时,这将导致资源泄漏。

Code
 var acquired = 0        class Resource {            init { acquired++ } // Acquire the resource            fun close() { acquired-- } // Release the resource        }         runBlocking {            repeat(10_000) { // Launch 10K coroutines                launch {                    val resource = withTimeout(60) { // Timeout of 60 ms                        delay(50) // Delay for 50 ms                        Resource() // Acquire a resource and return it from withTimeout block                    }                    resource.close() // Release the resource                }            }        }        // Outside of runBlocking all coroutines have completed        println(acquired) // Print the number of resources still acquired

如果运行上面的代码,您会发现它并不总是打印零,尽管它可能取决于您的机器的计时。您可能需要调整此示例中的超时才能实际看到非零值。

要解决此问题,您可以将资源的引用存储在变量中,而不是从 withTimeout 块返回它。

Code
var acquired = 0 class Resource {    init { acquired++ } // Acquire the resource    fun close() { acquired-- } // Release the resource} fun main() {//sampleStart    runBlocking {        repeat(10_000) { // Launch 10K coroutines            launch {                 var resource: Resource? = null // Not acquired yet                try {                    withTimeout(60) { // Timeout of 60 ms                        delay(50) // Delay for 50 ms                        resource = Resource() // Store a resource to the variable if acquired                          }                    // We can do something else with the resource here                } finally {                      resource?.close() // Release the resource if it was acquired                }            }        }    }    // Outside of runBlocking all coroutines have completed    println(acquired) // Print the number of resources still acquired

此示例始终打印零,资源不泄露。

参考文档