kotlin-協(xié)程

Why

  • 簡化異步代碼的編寫。
  • 執(zhí)行嚴(yán)格主線程安全確保你的代碼永遠(yuǎn)不會意外阻塞主線程,并增強(qiáng)了代碼的可讀性。
  • 提升代碼安全性,不會發(fā)生內(nèi)存泄露。
  • 協(xié)程間通信。

What

協(xié)程的概念在編程語言的早期就出現(xiàn)了,在1967年Simula第一次使用協(xié)程。
協(xié)程就像非常輕量級的線程。
線程是由系統(tǒng)調(diào)度的,線程切換或線程阻塞的開銷都比較大。而協(xié)程依賴于線程,但是協(xié)程掛起時(shí)不需要阻塞線程,幾乎是無代價(jià)的,協(xié)程是由開發(fā)者控制的。所以協(xié)程也像用戶態(tài)的線程,非常輕量級,一個(gè)線程中可以創(chuàng)建任意個(gè)協(xié)程。

舉個(gè)通俗易懂的栗子,不一定準(zhǔn)確。假如要從 地鐵A站地鐵C站 看和一個(gè)妹子約會,但是當(dāng)?shù)竭_(dá) 地鐵B站 的時(shí)候,你想來想去應(yīng)該去給妹子買個(gè)精美的禮物,大概要1個(gè)小時(shí),而從 A站C站 只有這一輛列車,只不過開的飛快,每10分鐘又回到 A站重新出發(fā),地鐵好比一條線程,你去買禮物回到B站好比一個(gè)任務(wù)。在同步阻塞的情況下,是你去買禮物這段時(shí)間,地鐵一直等你,直到你帶著禮物回來。在有協(xié)程的情況下,你去買禮物好比一段協(xié)程,地鐵把你在B站放下(掛起),地鐵繼續(xù)開,你買好禮物了就在B站等下趟地鐵來,繼續(xù)上車(恢復(fù))前去約妹子。在異步的情況下是,你去買禮物(異步任務(wù)),地鐵繼續(xù)往前開,但是地鐵司機(jī)給你一個(gè)電話號碼(callback),你買禮物回到B站的時(shí)候需要打我的電話號碼,才讓你上車。異步callback的時(shí)候有個(gè)問題,每個(gè)人下車去臨時(shí)辦事司機(jī)還要給他一個(gè)電話號碼,如果他出異常不回來了,可能會導(dǎo)致司機(jī)的電話號碼泄露,非常不安全。

How

在Android開發(fā)中,經(jīng)常遇到的問題:

  • Long running tasks
  • Main-safety
  • Leak work

Long running tasks

  • 一次CPU循環(huán)小于0.0000000004秒
  • 一次網(wǎng)絡(luò)請求大約0.4秒

在Android中主線程主要用戶UI的渲染和響應(yīng)用戶手勢交互,以及輕量級的邏輯運(yùn)算。若果在主線程發(fā)起一個(gè)請求,將會導(dǎo)致應(yīng)用變慢、變卡、無法響應(yīng)用戶的交互,很容易造成ARN,用戶體驗(yàn)極差。所以業(yè)界通行的做法是通過callback實(shí)現(xiàn)異步回調(diào):

class ViewModel: ViewModel() {
   fun fetchDocs() {
       get("developer.android.com") { result ->
           show(result)
       }
    }
}

上面callback的示例只是一層回調(diào)的情況,假如有兩個(gè)甚至更多的異步請求,而且存在下一個(gè)請求依賴上一個(gè)請求的結(jié)果,就會存在層層嵌套,當(dāng)然目前比較流行的做法是用Retrofit的轉(zhuǎn)換函數(shù)flatMap實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用,但是代碼看起來還是很臃腫。如果使用協(xié)程上面的代碼可以簡化成這樣:

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.Main
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}

suspend fun get(url: String) = withContext(Dispatchers.IO) {
    // Make a request
    // Dispatchers.IO
}

Coroutines提供一個(gè)很好途徑可以簡化耗時(shí)任務(wù)的代碼編寫,使得異步callback的代碼可以像同步代碼一樣順序編寫。Coroutines在普通的方法上面加上兩個(gè)新的操作。除了callreturn,Coroutines還增加 suspendresume
協(xié)程使用棧幀管理當(dāng)前運(yùn)行的方法和方法的所有本地變量。當(dāng)協(xié)程開始掛起,當(dāng)前棧幀被復(fù)制并保存以供后續(xù)使用。當(dāng)協(xié)程開始被恢復(fù),棧幀將從它被保存的地方恢復(fù)回來,當(dāng)前棧幀的方法繼續(xù)執(zhí)行。

  • suspend: 掛起當(dāng)前協(xié)程的執(zhí)行,將當(dāng)前執(zhí)行棧幀的所有本地變量和函數(shù)copy出來并保存。
  • resume: 從掛起的地方繼續(xù)當(dāng)前協(xié)程的執(zhí)行。

suspend functions只能在協(xié)程或者suspend functions 中被調(diào)用。

Main-safety with coroutines

在Kotlin協(xié)程中,寫的好的suspend functions 總是應(yīng)該可以安全的從主線程被調(diào)用,也應(yīng)該允許從任何線程被調(diào)用。使用suspend修飾的 function并不是告訴Kotlin這個(gè)方法在主線程運(yùn)行。

為了寫一個(gè)主線程安全的耗時(shí)方法,你可以讓協(xié)程在Default 或者 IO dispatcher中執(zhí)行(用withContext(Dispatchers.IO)指定在IO線程中運(yùn)行)。在協(xié)程所有的協(xié)程必須運(yùn)行在dispatcher中,即使他們運(yùn)行在主線程中。Coroutines將會掛起自己,dispatcher知道如何恢復(fù)他們。

為了指定coroutines在什么線程運(yùn)行,kotlin提供了四種Dispatchers:

Dispatchers 用途 使用場景
Dispatchers.Main 主線程,和UI交互,執(zhí)行輕量任務(wù) 1.call suspend functions。2. call UI functions。 3. Update LiveData
Dispatchers.IO 用于網(wǎng)絡(luò)請求和文件訪問 1. Database。 2.Reading/writing files。3. Networking
Dispatchers.Default CPU密集型任務(wù) 1. Sorting a list。 2.Parsing JSON。 3.DiffUtils
Dispatchers.Unconfined 不限制任何制定線程 高級調(diào)度器,不應(yīng)該在常規(guī)代碼里使用

假如你在Room中使用suspend functionsRxJavaLiveData,它自動提供了主線程安全。

// Dispatchers.Main
suspend fun fetchDocs() {
    // Dispatchers.Main
    val result = get("developer.android.com")
    // Dispatchers.Main
    show(result)
}
// Dispatchers.Main
suspend fun get(url: String) =
    // Dispatchers.IO
    withContext(Dispatchers.IO) {
        // Dispatchers.IO
        /* perform blocking network IO here */
    }
    // Dispatchers.Main

Leak work

你的程序里面可能會有成千上萬個(gè)協(xié)程,你很難通過代碼手動追蹤它們,假如你通過代碼手動追蹤他們以確保它們完成或取消,那么代碼會顯得臃腫且很容易出錯。如果代碼不是很完美,可能會失去對coroutine的追蹤,并導(dǎo)致任務(wù)泄露。任務(wù)泄露就像內(nèi)存泄露,但是更嚴(yán)重。它不但會浪費(fèi)內(nèi)存的使用,還有cpu、磁盤,甚至?xí)l(fā)起一個(gè)網(wǎng)絡(luò)請求。

在android中,我們知道Activity和Fragment等都是有生命周期的,我們通常的模式是當(dāng)前頁面退出的時(shí)候,取消所有的異步任務(wù)。假如有一個(gè)異步的網(wǎng)絡(luò)請求,在當(dāng)前頁面銷毀的時(shí)候還在執(zhí)行,會導(dǎo)致哪些問題:

  • 空指針異常。為請求結(jié)果回來之后去更新UI狀態(tài),而意外導(dǎo)致空指針異常。
  • 浪費(fèi)內(nèi)存資源。
  • 浪費(fèi)CPU資源。

為了避免協(xié)程泄露,kotlin引入 結(jié)構(gòu)化并發(fā) 。結(jié)構(gòu)化并發(fā)是語言特性和最佳實(shí)踐的組合,如果我們遵循最佳實(shí)踐,將幫助追蹤運(yùn)行在協(xié)程中的任務(wù)。在Android中結(jié)構(gòu)化并發(fā)可以幫我們做如下三件事:

  • 取消任務(wù),當(dāng)協(xié)程不再需要的時(shí)候。
  • 追蹤任務(wù),當(dāng)協(xié)程運(yùn)行的時(shí)候。
  • 傳播錯誤信號,當(dāng)協(xié)程執(zhí)行失敗的時(shí)候。

解決方式:

  • CoroutineScope 取消任務(wù),其實(shí)是通過關(guān)聯(lián)的job取消任務(wù)。
  • 任務(wù)追蹤,coroutines的結(jié)構(gòu)化并發(fā)通過coroutineScopesupervisorScope 保證 suspend function的所有任務(wù)完成才返回。
  • 傳播錯誤信號coroutineScope 保證錯誤雙向傳遞,只要有一個(gè)子coroutine失敗或出現(xiàn)異常,異常往父域傳遞,并取消所有的子coroutines。而 supervisorScope 實(shí)現(xiàn)單向錯誤傳遞,適用于作業(yè)監(jiān)控器。
CoroutineScope

在kotlin中所有的協(xié)程必須運(yùn)行在CoroutineScope中,scope幫你追蹤所有協(xié)程的狀態(tài),但是它不像Dispatcher,并不運(yùn)行你的協(xié)程。它可以取消所有在里面啟動的協(xié)程。啟動一個(gè)新協(xié)程:

scope.launch {
    // This block starts a new coroutine 
    // "in" the scope.
    // 
    // It can call suspend functions
   fetchDocs()
}

創(chuàng)建CoroutineScope的常見方式如下:

  • CoroutineScope(context: CoroutineContext),api方法,如:val scope = CoroutineScope(Dispatchers.Main + Job()),或者如下:
class LifecycleCoroutineScope : CoroutineScope, Closeable {

    private val job = JobSupervisorJob()
    override val coroutineContext: CoroutineContext
        get() = Dispatchers.Main + job

    override fun close() {
        job.cancel()
    }
}
class SimpleRetrofitActivity : FragmentActivity() {
    private val activityScope = LifecycleCoroutineScope()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_simple_retrofit)

        // some other code ...
    }

    override fun onDestroy() {
        super.onDestroy()
        activityScope.close()
    }
    // some other code ...
}
  • coroutineScope:api方法,創(chuàng)建新一個(gè)子域,并管理域中的所有協(xié)程。注意這個(gè)方法只有在block中創(chuàng)建的所有子協(xié)程全部執(zhí)行完畢后,才會退出。
  • supervisorScope:與 coroutineScope的區(qū)別是在子協(xié)程失敗時(shí),錯誤不會往上傳遞給父域,所以不會影響子協(xié)程。

創(chuàng)建協(xié)程的常見方式如下:

  • lauch:協(xié)程構(gòu)建器,創(chuàng)建并啟動(也可以延時(shí)啟動)一個(gè)協(xié)程,返回一個(gè)Job,用于監(jiān)督和取消任務(wù),用于無返回值的場景。
  • async:協(xié)程構(gòu)建器,和launch一樣,區(qū)別是返回一個(gè)Job的子類 Deferred,唯一的區(qū)別是可以通過await獲取完成時(shí)的返回值,或者捕獲異常(異常處理也不一樣)。

在Android中有一個(gè)kotlin的ViewModel的擴(kuò)展庫 lifecycle-viewmodel-ktx:2.1.0-alpha04,可以通過viewModelScope擴(kuò)展屬性啟動協(xié)程,viewModelScope綁定了activity的生命周期,activity銷毀的時(shí)候會自動取消在這個(gè)scope中啟動的所有協(xié)程。

fun runForever() {
    // start a new coroutine in the ViewModel
    viewModelScope.launch {
        // cancelled when the ViewModel is cleared
        while(true) {
            delay(1_000)
            // do something every second
        }
    }
}

注意,協(xié)程的取消是協(xié)作的,當(dāng)協(xié)程掛起的時(shí)候被取消將會拋一個(gè) CancellationException,即使你捕獲了這個(gè)異常,這個(gè)協(xié)程的狀態(tài)也變?yōu)槿∠麪顟B(tài)。假如你是一個(gè)計(jì)算協(xié)程,并且沒有檢查取消狀態(tài),那么這個(gè)協(xié)程不能被取消。

val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
    var nextPrintTime = startTime
    var i = 0
    while (i < 5) { // computation loop, just wastes CPU
        // print a message twice a second
        if (System.currentTimeMillis() >= nextPrintTime) {
            println("job: I'm sleeping ${i++} ...")
            nextPrintTime += 500L
        }
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
任務(wù)追蹤

有時(shí),我們希望兩個(gè)或多個(gè)請求同時(shí)并發(fā),并等待他們?nèi)客瓿桑?code>suspend function 加上 coroutineScope 創(chuàng)建的子域可以保證全部子協(xié)程完成才返回。

suspend fun fetchTwoDocs() {
    coroutineScope {
        launch { fetchDoc(1) }
        async { fetchDoc(2) }
    }
}
傳播錯誤信號

注意協(xié)程的結(jié)構(gòu)化并發(fā)是基于語言特性加上最佳實(shí)踐的,如下方式會導(dǎo)致,錯誤丟失:

val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
    // async without structured concurrency
    unrelatedScope.async {
        throw InAsyncNoOneCanHearYou("except")
    }
}

上面代碼丟失錯誤是因?yàn)?async的恢復(fù)需要調(diào)用await,這樣才能將異常重新上傳,而在suspend function 使用了另外一個(gè)協(xié)程域,導(dǎo)致lostError不會等待自作業(yè)的完成就退出了。正確的結(jié)構(gòu)化并發(fā):

suspend fun foundError() {
    coroutineScope {
        async { 
            throw StructuredConcurrencyWill("throw")
        }
    }
}

你可以通過CoroutineScope (注意是大寫開頭的C) 和 GlobalScope來創(chuàng)建 非結(jié)構(gòu)化的協(xié)程,僅僅當(dāng)你認(rèn)為它的生命周期比調(diào)用者生命周期更長。

總結(jié)

  • CoroutineScope:協(xié)程作用域包含 CoroutineContext,用于啟動協(xié)程,并追蹤子協(xié)程,其實(shí)是通過Job追蹤的。
  • CoroutineContext:協(xié)程上下文,主要包含JobCoroutineDispatcher,表示一個(gè)協(xié)程的場景。
  • CoroutineDispatcher:協(xié)程調(diào)度器,決定協(xié)程所在的線程或線程池。它可以指定協(xié)程運(yùn)行于特定的一個(gè)線程、一個(gè)線程池或者不指定任何線程。
  • Job:任務(wù),封裝了協(xié)程中需要執(zhí)行的代碼邏輯。Job 可以取消并且有簡單生命周期,它有三種狀態(tài):isActiveisCompletedisCancelled
  • Deferred:Job的子類,有返回值的Job,通過await獲取。
  • 協(xié)程構(gòu)建器包括:lauchasync

示例

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,488評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,034評論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,327評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,554評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,337評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,883評論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,975評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,114評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,625評論 1 332
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,555評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,737評論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,244評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,973評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,615評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,343評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,699評論 2 370