github原文地址
原創(chuàng)翻譯,轉載請保留或注明出處:http://www.reibang.com/p/971f929f9bf5
協(xié)程上下文和調度器
協(xié)程總是在一些由kotlin標準庫中定義的 CoroutineContext 類型值表示的上下文中執(zhí)行。
協(xié)程上下文是一組不同的元素喷楣。主要元素是我們之前見過的協(xié)程的 Job 熄赡,以及本節(jié)討論的調度器。
調度器和線程
協(xié)程上下文包括一個協(xié)程調度程序(參見 CoroutineDispatcher ),它確定對應協(xié)程的執(zhí)行線程拭荤。協(xié)程調度器可以將協(xié)程的執(zhí)行限制在一個特定的線程內,調度它到一個線程池震叮,或者讓它不受限制的運行沿量。
所有協(xié)程構建器(如 launch 和 async )都接受可選的 CoroutineContext 參數(shù),該參數(shù)可用于為新協(xié)程和其他上下文元素顯式指定調度器冤荆。
嘗試以下示例:
fun main(args: Array<String>) = runBlocking<Unit> {
val jobs = arrayListOf<Job>()
jobs += launch(Unconfined) { // not confined -- will work with main thread
println(" 'Unconfined': I'm working in thread ${Thread.currentThread().name}")
}
jobs += launch(coroutineContext) { // context of the parent, runBlocking coroutine
println("'coroutineContext': I'm working in thread ${Thread.currentThread().name}")
}
jobs += launch(CommonPool) { // will get dispatched to [ForkJoinPool.commonPool](http://forkjoinpool.commonpool/) (or equivalent)
println(" 'CommonPool': I'm working in thread ${Thread.currentThread().name}")
}
jobs += launch(newSingleThreadContext("MyOwnThread")) { // will get its own new thread
println(" 'newSTC': I'm working in thread ${Thread.currentThread().name}")
}
jobs.forEach { it.join() }
}
獲取完整代碼 here
輸出如下(也許以不同的順序):
'Unconfined': I'm working in thread main
'CommonPool': I'm working in thread [ForkJoinPool.commonPool-worker-1](http://forkjoinpool.commonpool-worker-1/)
'newSTC': I'm working in thread MyOwnThread
'coroutineContext': I'm working in thread main
我們在前面小節(jié)中使用的默認調度器是 DefaultDispatcher 表示的朴则,它等同于當前實現(xiàn)中的 CommonPool 。所以launch { ... }
等同于launch(DefaultDispatcher) { ... }
钓简,等同于launch(CommonPool) { ... }
乌妒。
父 coroutineContext 和 Unconfined 上下文之間的區(qū)別將在稍后顯示。
請注意一點外邓,newSingleThreadContext 會創(chuàng)建一個新線程撤蚊,這是一個非常昂貴的資源。在真實環(huán)境的應用程序中损话,它必須被釋放掉侦啸,不再需要時,使用 close 函數(shù)丧枪,或者存在頂層變量中光涂,并在整個應用程序中重用。
非受限 vs 受限 調度器
非受限協(xié)程調度器在調用者線程中啟動協(xié)程拧烦,但僅限于第一個掛起點忘闻。在暫停之后,它將在掛起函數(shù)被調用的完全確定的線程中恢復恋博。當協(xié)程不消耗CPU時間或者更新受限于特定線程的任何共享數(shù)據(jù)(如UI)時齐佳,非受限調度器是合適的。
另一方面债沮,coroutineContext 屬性(在任何協(xié)程中可用)炼吴,都是對此特定協(xié)程上下文的引用。這樣的話疫衩,父上下文可以被繼承硅蹦。runBlocking 協(xié)程的默認調度器,特別受限于調用者線程隧土。因此繼承它的總用是通過可預測的先進先出調度將執(zhí)行限制在該線程中提针。
fun main(args: Array<String>) = runBlocking<Unit> {
val jobs = arrayListOf<Job>()
jobs += launch(Unconfined) { // not confined -- will work with main thread
println(" 'Unconfined': I'm working in thread ${Thread.currentThread().name}")
delay(500)
println(" 'Unconfined': After delay in thread ${Thread.currentThread().name}")
}
jobs += launch(coroutineContext) { // context of the parent, runBlocking coroutine
println("'coroutineContext': I'm working in thread ${Thread.currentThread().name}")
delay(1000)
println("'coroutineContext': After delay in thread ${Thread.currentThread().name}")
}
jobs.forEach { it.join() }
}
獲取完整代碼 here
輸出如下:
'Unconfined': I'm working in thread main
'coroutineContext': I'm working in thread main
'Unconfined': After delay in thread kotlinx.coroutines.DefaultExecutor
'coroutineContext': After delay in thread main
因此,繼承了(來自runBlocking {...}
協(xié)程的)coroutineContext
的協(xié)程在主線程中繼續(xù)執(zhí)行曹傀,而非受限協(xié)程在 delay 函數(shù)正在使用的默認執(zhí)行線程中恢復辐脖。
調試協(xié)程和線程
協(xié)程可以在一個線程上掛起,并在另一個具有非受限調度器或默認多線程調度器的線程上掛起皆愉。即便具有一個單線程調度器嗜价,弄清楚什么協(xié)程艇抠、何時、何處執(zhí)行也是很困難的久锥。調試具有線程的應用的常用方式是在沒條日志語句中打印線程名稱家淤。這個特性得到日志框架的普遍支持。當使用協(xié)程時瑟由,只有線程名稱不會提供更多的上下文絮重,所以kotlinx.coroutines
包含的調試工具讓事情變得簡單起來。
使用-Dkotlinx.coroutines.debug
JVM參數(shù)運行以下代碼:
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
fun main(args: Array<String>) = runBlocking<Unit> {
val a = async(coroutineContext) {
log("I'm computing a piece of the answer")
6
}
val b = async(coroutineContext) {
log("I'm computing another piece of the answer")
7
}
log("The answer is ${a.await() * b.await()}")
}
獲取完整代碼 here
存在三個協(xié)程歹苦。一個runBlocking
主協(xié)程(#1) , 以及兩個計算延遲值的協(xié)程——a
(#2) 和b
(#3)青伤。 它們都運行在runBlocking
的上下文中,并且限制在主線程中殴瘦。這段代碼輸出如下:
[main @coroutine#2] I'm computing a piece of the answer
[main @coroutine#3] I'm computing another piece of the answer
[main @coroutine#1] The answer is 42
log
函數(shù)在方括號中打印線程的名稱狠角,你可以看到它是main
線程,但當前正在執(zhí)行的協(xié)程的標識符附加到了后面蚪腋。在打開調試模式時丰歌,此標識符會連續(xù)地分配給所有創(chuàng)建的協(xié)程。
你可以在 newCoroutineContext 函數(shù)的文檔中閱讀有關調試工具的更多信息屉凯。
在線程間跳躍
使用-Dkotlinx.coroutines.debug
JVM參數(shù)運行以下代碼:
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
fun main(args: Array<String>) {
newSingleThreadContext("Ctx1").use { ctx1 ->
newSingleThreadContext("Ctx2").use { ctx2 ->
runBlocking(ctx1) {
log("Started in ctx1")
withContext(ctx2) {
log("Working in ctx2")
}
log("Back to ctx1")
}
}
}
}
獲取完整代碼 here
這里演示了幾種新技術立帖。一個是使用具有明確指定上下文的 runBlocking ,另一個是使用 withContext 函數(shù)改變協(xié)程的上下文神得,同時仍舊停留在相同的協(xié)程中厘惦,如下面的輸出中所示:
[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1
請注意,該示例還使用了Kotlin標準庫中的use
函數(shù)來釋放使用了 newSingleThreadContext 創(chuàng)建的線程哩簿,當它們不再被需要時。
上下文中的Job
協(xié)程的 Job 是其上下文的一部分酝静,協(xié)程可以使用coroutineContext[Job]
表達式從它自己的上下文中拿到 Job :
fun main(args: Array<String>) = runBlocking<Unit> {
println("My job is ${coroutineContext[Job]}")
}
獲取完整代碼 here
當運行在 debug mode 中节榜,輸出如下:
My job is "coroutine#1":BlockingCoroutine{Active}@6d311334
所以在 CoroutineScope 中的 isActive 只是coroutineContext[Job]?.isActive == true
的一個方便的快捷方式。
子協(xié)程
當一個協(xié)程的 coroutineContext 被用來啟動另一個協(xié)程别智,新協(xié)程的 Job 成為父協(xié)程的一個子Job宗苍,當父協(xié)程被取消時,所有它的子協(xié)程也會被遞歸取消薄榛。
fun main(args: Array<String>) = runBlocking<Unit> {
// launch a coroutine to process some kind of incoming request
val request = launch {
// it spawns two other jobs, one with its separate context
val job1 = launch {
println("job1: I have my own context and execute independently!")
delay(1000)
println("job1: I am not affected by cancellation of the request")
}
// and the other inherits the parent context
val job2 = launch(coroutineContext) {
delay(100)
println("job2: I am a child of the request coroutine")
delay(1000)
println("job2: I will not execute this line if my parent request is cancelled")
}
// request completes when both its sub-jobs complete:
job1.join()
job2.join()
}
delay(500)
request.cancel() // cancel processing of the request
delay(1000) // delay a second to see what happens
println("main: Who has survived request cancellation?")
}
獲取完整代碼 here
這段代碼輸出如下:
job1: I have my own context and execute independently!
job2: I am a child of the request coroutine
job1: I am not affected by cancellation of the request
main: Who has survived request cancellation?
結合上下文
協(xié)程上下文可以通過 +
操作符結合讳窟。右側的上下文替換了左側上下文的相關條目。例如敞恋,一個 父協(xié)程的 Job 可以被繼承丽啡,同時替換它的調度器:
fun main(args: Array<String>) = runBlocking<Unit> {
// start a coroutine to process some kind of incoming request
val request = launch(coroutineContext) { // use the context of `runBlocking`
// spawns CPU-intensive child job in CommonPool !!!
val job = launch(coroutineContext + CommonPool) {
println("job: I am a child of the request coroutine, but with a different dispatcher")
delay(1000)
println("job: I will not execute this line if my parent request is cancelled")
}
job.join() // request completes when its sub-job completes
}
delay(500)
request.cancel() // cancel processing of the request
delay(1000) // delay a second to see what happens
println("main: Who has survived request cancellation?")
}
獲取完整代碼 here
這段代碼的預期輸出如下:
job: I am a child of the request coroutine, but with a different dispatcher
main: Who has survived request cancellation?
父協(xié)程的職責
一個父協(xié)程總是等待其全部子協(xié)程的完成。而且并不需要顯示地追蹤它啟動的所有子協(xié)程硬猫,也不必使用 Job.join 在最后等待它們:
fun main(args: Array<String>) = runBlocking<Unit> {
// launch a coroutine to process some kind of incoming request
val request = launch {
repeat(3) { i -> // launch a few children jobs
launch(coroutineContext) {
delay((i + 1) * 200L) // variable delay 200ms, 400ms, 600ms
println("Coroutine $i is done")
}
}
println("request: I'm done and I don't explicitly join my children that are still active")
}
request.join() // wait for completion of the request, including all its children
println("Now processing of the request is complete")
}
獲取完整代碼 here
結果將是:
request: I'm done and I don't explicitly join my children that are still active
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done
Now processing of the request is complete
為調試命名協(xié)程
當協(xié)程經(jīng)常產(chǎn)生日志時补箍,自動分配的id是可以接受的改执,你只需要關聯(lián)這些來自同一協(xié)程的日志記錄。然而當協(xié)程與特定請求的處理或特定的后臺任務相關時坑雅,最好為其明確地命名以方便調試辈挂。CoroutineName 上下文與線程名具有相同的功能。當debugging mode 開啟后裹粤,它將顯示在執(zhí)行此協(xié)程的線程名稱中终蒂。
以下示例示范了此概念:
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")
fun main(args: Array<String>) = runBlocking(CoroutineName("main")) {
log("Started main coroutine")
// run two background value computations
val v1 = async(CoroutineName("v1coroutine")) {
delay(500)
log("Computing v1")
252
}
val v2 = async(CoroutineName("v2coroutine")) {
delay(1000)
log("Computing v2")
6
}
log("The answer for v1 / v2 = ${v1.await() / v2.await()}")
}
獲取完整代碼 here
帶有 -Dkotlinx.coroutines.debug
JVM 選項的輸出類似于:
[main @main#1] Started main coroutine
[[ForkJoinPool.commonPool-worker-1](http://forkjoinpool.commonpool-worker-1/) @v1coroutine#2] Computing v1
[[ForkJoinPool.commonPool-worker-2](http://forkjoinpool.commonpool-worker-2/) @v2coroutine#3] Computing v2
[main @main#1] The answer for v1 / v2 = 42
明確地取消指定job
讓我們把上下文、父子和jobs的認知合起來遥诉。假設一個應用程序拇泣,它擁有一個具有生命周期的對象,但該對象不是一個協(xié)程突那。例如挫酿,我們正在編寫一個Android應用,并在Android activity的上下文中啟動了各種協(xié)程用來執(zhí)行異步操作愕难,比如獲取早龟、更新數(shù)據(jù)和執(zhí)行動畫等等。當activity被銷毀時所有這些協(xié)程必須被取消猫缭,以避免內存泄漏葱弟。
我們可以通過創(chuàng)建與activity生命周期相關聯(lián)的 Job 的實例來管理協(xié)程的生命周期。一個job實例可以通過 Job() 工廠函數(shù)創(chuàng)建猜丹,如以下示例所示芝加。為方便起見,我們可以編寫 launch(coroutineContext, parent = job)
來明確表示正在使用父job這一事實射窒,而不是使用 launch(coroutineContext + job)
表達式藏杖。
現(xiàn)在,Job.cancel 的單個調用取消了我們啟動的所有子協(xié)程脉顿。此外蝌麸,Job.join 等待所有子協(xié)程的完成,所以我們也可以在這個例子中使用 cancelAndJoin:
fun main(args: Array<String>) = runBlocking<Unit> {
val job = Job() // create a job object to manage our lifecycle
// now launch ten coroutines for a demo, each working for a different time
val coroutines = List(10) { i ->
// they are all children of our job object
launch(coroutineContext, parent = job) { // we use the context of main runBlocking thread, but with our parent job
delay((i + 1) * 200L) // variable delay 200ms, 400ms, ... etc
println("Coroutine $i is done")
}
}
println("Launched ${coroutines.size} coroutines")
delay(500L) // delay for half a second
println("Cancelling the job!")
job.cancelAndJoin() // cancel all our coroutines and wait for all of them to complete
}
獲取完整代碼 here
這個示例的輸出如下:
Launched 10 coroutines
Coroutine 0 is done
Coroutine 1 is done
Cancelling the job!
正如你所見艾疟,只有前三個協(xié)程打印了一條消息来吩,而其他協(xié)程被一次job.cancelAndJoin()
調用取消掉了。所以在我們假設的Android應用中蔽莱,我們需要做的是當activity被創(chuàng)建時同時創(chuàng)建一個父job對象弟疆,將它用于子協(xié)程,并在activity銷毀時取消它盗冷。我們無法在Android的生命周期中join 它們怠苔,因為它是同步的。但是這種join 的能力在構建后端服務時是很有用的正塌,用來確保有限資源的使用嘀略。