【翻譯】kotlin協(xié)程核心庫文檔(四)—— 協(xié)程上下文和調度器

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é)程構建器(如 launchasync )都接受可選的 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) { ... }乌妒。

coroutineContextUnconfined 上下文之間的區(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.debugJVM參數(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.debugJVM參數(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 的能力在構建后端服務時是很有用的正塌,用來確保有限資源的使用嘀略。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末恤溶,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子帜羊,更是在濱河造成了極大的恐慌咒程,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讼育,死亡現(xiàn)場離奇詭異帐姻,居然都是意外死亡,警方通過查閱死者的電腦和手機奶段,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進店門饥瓷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人痹籍,你說我怎么就攤上這事呢铆。” “怎么了蹲缠?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵棺克,是天一觀的道長。 經(jīng)常有香客問我线定,道長娜谊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任斤讥,我火速辦了婚禮纱皆,結果婚禮上,老公的妹妹穿的比我還像新娘芭商。我一直安慰自己派草,他們只是感情好,可當我...
    茶點故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布铛楣。 她就那樣靜靜地躺著澳眷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蛉艾。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天衷敌,我揣著相機與錄音勿侯,去河邊找鬼。 笑死缴罗,一個胖子當著我的面吹牛助琐,可吹牛的內容都是我干的。 我是一名探鬼主播面氓,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼兵钮,長吁一口氣:“原來是場噩夢啊……” “哼蛆橡!你這毒婦竟也來了?” 一聲冷哼從身側響起掘譬,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤泰演,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后葱轩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體睦焕,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年靴拱,在試婚紗的時候發(fā)現(xiàn)自己被綠了垃喊。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡袜炕,死狀恐怖本谜,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情偎窘,我是刑警寧澤乌助,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站评架,受9級特大地震影響眷茁,放射性物質發(fā)生泄漏。R本人自食惡果不足惜纵诞,卻給世界環(huán)境...
    茶點故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一上祈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧浙芙,春花似錦登刺、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至南窗,卻和暖如春揍很,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背万伤。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工窒悔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人敌买。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓简珠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親虹钮。 傳聞我的和親對象是個殘疾皇子聋庵,可洞房花燭夜當晚...
    茶點故事閱讀 42,792評論 2 345