Kotlin Coroutines(協(xié)程) 完全解析(一),協(xié)程簡(jiǎn)介

Kotlin Coroutines(協(xié)程) 完全解析系列:

Kotlin Coroutines(協(xié)程) 完全解析(一)袱瓮,協(xié)程簡(jiǎn)介

Kotlin Coroutines(協(xié)程) 完全解析(二),深入理解協(xié)程的掛起、恢復(fù)與調(diào)度

Kotlin Coroutines(協(xié)程) 完全解析(三)鸠窗,封裝異步回調(diào)、協(xié)程間關(guān)系及協(xié)程的取消

Kotlin Coroutines(協(xié)程) 完全解析(四)胯究,協(xié)程的異常處理

Kotlin Coroutines(協(xié)程) 完全解析(五)稍计,協(xié)程的并發(fā)

本文基于 Kotlin v1.3.0-rc-146,Kotlin-Coroutines v1.0.0-RC1

Kotlin 中引入 Coroutine(協(xié)程) 的概念裕循,可以幫助編寫異步代碼臣嚣,目前還是試驗(yàn)性的。國(guó)內(nèi)詳細(xì)介紹協(xié)程的資料比較少剥哑,所以我打算寫 Kotlin Coroutines(協(xié)程) 完全解析的系列文章硅则,希望可以幫助大家更好地理解協(xié)程。這是系列文章的第一篇星持,簡(jiǎn)單介紹協(xié)程的特點(diǎn)和一些基本概念抢埋。協(xié)程主要的目的是簡(jiǎn)化異步編程,那么先從為什么需要協(xié)程來編寫異步代碼開始督暂。

Kotlin Coroutine 終于正式發(fā)布了揪垄,所以我跟進(jìn)最新的正式版更新了本文相關(guān)內(nèi)容

1. 為什么需要協(xié)程?

異步編程中最為常見的場(chǎng)景是:在后臺(tái)線程執(zhí)行一個(gè)復(fù)雜任務(wù)逻翁,下一個(gè)任務(wù)依賴于上一個(gè)任務(wù)的執(zhí)行結(jié)果饥努,所以必須等待上一個(gè)任務(wù)執(zhí)行完成后才能開始執(zhí)行“嘶兀看下面代碼中的三個(gè)函數(shù)酷愧,后兩個(gè)函數(shù)都依賴于前一個(gè)函數(shù)的執(zhí)行結(jié)果驾诈。

fun requestToken(): Token {
    // makes request for a token & waits
    return token // returns result when received 
}

fun createPost(token: Token, item: Item): Post {
    // sends item to the server & waits
    return post // returns resulting post 
}

fun processPost(post: Post) {
    // does some local processing of result
}

三個(gè)函數(shù)中的操作都是耗時(shí)操作,因此不能直接在 UI 線程中運(yùn)行溶浴,而且后兩個(gè)函數(shù)都依賴于前一個(gè)函數(shù)的執(zhí)行結(jié)果乍迄,三個(gè)任務(wù)不能并行運(yùn)行,該如何解決這個(gè)問題呢士败?

1.1 回調(diào)

常見的做法是使用回調(diào)闯两,把之后需要執(zhí)行的任務(wù)封裝為回調(diào)。

fun requestTokenAsync(cb: (Token) -> Unit) { ... }
fun createPostAsync(token: Token, item: Item, cb: (Post) -> Unit) { ... }
fun processPost(post: Post) { ... }

fun postItem(item: Item) {
    requestTokenAsync { token ->
        createPostAsync(token, item) { post ->
            processPost(post)
        }
    }
}

回調(diào)在只有兩個(gè)任務(wù)的場(chǎng)景是非常簡(jiǎn)單實(shí)用的谅将,很多網(wǎng)絡(luò)請(qǐng)求框架的 onSuccess Listener 就是使用回調(diào)漾狼,但是在三個(gè)以上任務(wù)的場(chǎng)景中就會(huì)出現(xiàn)多層回調(diào)嵌套的問題,而且不方便處理異常饥臂。

1.2 Future

Java 8 引入的 CompletableFuture 可以將多個(gè)任務(wù)串聯(lián)起來逊躁,可以避免多層嵌套的問題。

fun requestTokenAsync(): CompletableFuture<Token> { ... }
fun createPostAsync(token: Token, item: Item): CompletableFuture<Post> { ... }
fun processPost(post: Post) { ... }

fun postItem(item: Item) {
    requestTokenAsync()
            .thenCompose { token -> createPostAsync(token, item) }
            .thenAccept { post -> processPost(post) }
            .exceptionally { e ->
                e.printStackTrace()
                null
            }
}

上面代碼中使用連接符串聯(lián)起三個(gè)任務(wù)隅熙,最后的exceptionally方法還可以統(tǒng)一處理異常情況稽煤,但是只能在 Java 8 以上才能使用。

1.3 Rx 編程

CompletableFuture 的方式有點(diǎn)類似 Rx 系列的鏈?zhǔn)秸{(diào)用猛们,這也是目前大多數(shù)推薦的做法念脯。

fun requestToken(): Token { ... }
fun createPost(token: Token, item: Item): Post { ... }
fun processPost(post: Post) { ... }

fun postItem(item: Item) {
    Single.fromCallable { requestToken() }
            .map { token -> createPost(token, item) }
            .subscribe(
                    { post -> processPost(post) }, // onSuccess
                    { e -> e.printStackTrace() } // onError
            )
}

RxJava 豐富的操作符、簡(jiǎn)便的線程調(diào)度弯淘、異常處理使得大多數(shù)人滿意,我也如此吉懊,但是還沒有更簡(jiǎn)潔易讀的寫法呢庐橙?

1.4 協(xié)程

下面是使用 Kotlin 協(xié)程的代碼:

suspend fun requestToken(): Token { ... }   // 掛起函數(shù)
suspend fun createPost(token: Token, item: Item): Post { ... }  // 掛起函數(shù)
fun processPost(post: Post) { ... }

fun postItem(item: Item) {
    GlobalScope.launch {
        val token = requestToken()
        val post = createPost(token, item)
        processPost(post)
        // 需要異常處理,直接加上 try/catch 語句即可
    }
}

使用協(xié)程后的代碼非常簡(jiǎn)潔借嗽,以順序的方式書寫異步代碼态鳖,不會(huì)阻塞當(dāng)前 UI 線程,錯(cuò)誤處理也和平常代碼一樣簡(jiǎn)單恶导。

2. 協(xié)程是什么

2.1 Gradle 引入

dependencies {
    // Kotlin
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"

    // Kotlin Coroutines
    compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0-RC1'
}

2.2 協(xié)程的定義

先看官方文檔的描述:

協(xié)程通過將復(fù)雜性放入庫(kù)來簡(jiǎn)化異步編程浆竭。程序的邏輯可以在協(xié)程中順序地表達(dá),而底層庫(kù)會(huì)為我們解決其異步性惨寿。該庫(kù)可以將用戶代碼的相關(guān)部分包裝為回調(diào)邦泄、訂閱相關(guān)事件、在不同線程(甚至不同機(jī)器)上調(diào)度執(zhí)行裂垦,而代碼則保持如同順序執(zhí)行一樣簡(jiǎn)單顺囊。

協(xié)程的開發(fā)人員 Roman Elizarov 是這樣描述協(xié)程的:協(xié)程就像非常輕量級(jí)的線程。線程是由系統(tǒng)調(diào)度的蕉拢,線程切換或線程阻塞的開銷都比較大特碳。而協(xié)程依賴于線程诚亚,但是協(xié)程掛起時(shí)不需要阻塞線程,幾乎是無代價(jià)的午乓,協(xié)程是由開發(fā)者控制的站宗。所以協(xié)程也像用戶態(tài)的線程,非常輕量級(jí)益愈,一個(gè)線程中可以創(chuàng)建任意個(gè)協(xié)程梢灭。

總而言之:協(xié)程可以簡(jiǎn)化異步編程,可以順序地表達(dá)程序腕唧,協(xié)程也提供了一種避免阻塞線程并用更廉價(jià)或辖、更可控的操作替代線程阻塞的方法 -- 協(xié)程掛起。

3. 協(xié)程的基本概念

下面通過上面協(xié)程的例子來介紹協(xié)程中的一些基本概念:

3.1 掛起函數(shù)

suspend fun requestToken(): Token { ... }   // 掛起函數(shù)
suspend fun createPost(token: Token, item: Item): Post { ... }  // 掛起函數(shù)
fun processPost(post: Post) { ... }

requestTokencreatePost函數(shù)前面有suspend修飾符標(biāo)記枣接,這表示兩個(gè)函數(shù)都是掛起函數(shù)颂暇。掛起函數(shù)能夠以與普通函數(shù)相同的方式獲取參數(shù)和返回值,但是調(diào)用函數(shù)可能掛起協(xié)程(如果相關(guān)調(diào)用的結(jié)果已經(jīng)可用但惶,庫(kù)可以決定繼續(xù)進(jìn)行而不掛起)耳鸯,掛起函數(shù)掛起協(xié)程時(shí),不會(huì)阻塞協(xié)程所在的線程膀曾。掛起函數(shù)執(zhí)行完成后會(huì)恢復(fù)協(xié)程县爬,后面的代碼才會(huì)繼續(xù)執(zhí)行。但是掛起函數(shù)只能在協(xié)程中或其他掛起函數(shù)中調(diào)用添谊。事實(shí)上财喳,要啟動(dòng)協(xié)程,至少要有一個(gè)掛起函數(shù)斩狱,它通常是一個(gè)掛起 lambda 表達(dá)式耳高。所以suspend修飾符可以標(biāo)記普通函數(shù)、擴(kuò)展函數(shù)和 lambda 表達(dá)式所踊。

掛起函數(shù)只能在協(xié)程中或其他掛起函數(shù)中調(diào)用泌枪,上面例子中launch函數(shù)就創(chuàng)建了一個(gè)協(xié)程。

fun postItem(item: Item) {
    GlobalScope.launch { // 創(chuàng)建一個(gè)新協(xié)程
        val token = requestToken()
        val post = createPost(token, item)
        processPost(post)
        // 需要異常處理秕岛,直接加上 try/catch 語句即可
    }
}

launch函數(shù):

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

從上面函數(shù)定義中可以看到協(xié)程的一些重要的概念:CoroutineContext碌燕、CoroutineDispatcher、Job继薛,下面來一一介紹這些概念修壕。

3.1 CoroutineScope 和 CoroutineContext

CoroutineScope,可以理解為協(xié)程本身惋增,包含了 CoroutineContext叠殷。

CoroutineContext,協(xié)程上下文诈皿,是一些元素的集合林束,主要包括 Job 和 CoroutineDispatcher 元素像棘,可以代表一個(gè)協(xié)程的場(chǎng)景。

EmptyCoroutineContext 表示一個(gè)空的協(xié)程上下文壶冒。

3.2 CoroutineDispatcher

CoroutineDispatcher缕题,協(xié)程調(diào)度器,決定協(xié)程所在的線程或線程池胖腾。它可以指定協(xié)程運(yùn)行于特定的一個(gè)線程烟零、一個(gè)線程池或者不指定任何線程(這樣協(xié)程就會(huì)運(yùn)行于當(dāng)前線程)。coroutines-core中 CoroutineDispatcher 有三種標(biāo)準(zhǔn)實(shí)現(xiàn)Dispatchers.Default咸作、Dispatchers.IO锨阿,Dispatchers.MainDispatchers.Unconfined,Unconfined 就是不指定線程记罚。

launch函數(shù)定義如果不指定CoroutineDispatcher或者沒有其他的ContinuationInterceptor墅诡,默認(rèn)的協(xié)程調(diào)度器就是Dispatchers.DefaultDefault是一個(gè)協(xié)程調(diào)度器桐智,其指定的線程為共有的線程池末早,線程數(shù)量至少為 2 最大與 CPU 數(shù)相同。

3.3 Job & Deferred

Job说庭,任務(wù)然磷,封裝了協(xié)程中需要執(zhí)行的代碼邏輯。Job 可以取消并且有簡(jiǎn)單生命周期刊驴,它有三種狀態(tài):

State [isActive] [isCompleted] [isCancelled]
New (optional initial state) false false false
Active (default initial state) true false false
Completing (optional transient state) true false false
Cancelling (optional transient state) false false true
Cancelled (final state) false true true
Completed (final state) false true false

Job 完成時(shí)是沒有返回值的姿搜,如果需要返回值的話,應(yīng)該使用 Deferred捆憎,它是 Job 的子類public interface Deferred<out T> : Job痪欲。

3.4 Coroutine builders

CoroutineScope.launch函數(shù)屬于協(xié)程構(gòu)建器 Coroutine builders,Kotlin 中還有其他幾種 Builders攻礼,負(fù)責(zé)創(chuàng)建協(xié)程。

3.4.1 CoroutineScope.launch {}

CoroutineScope.launch {} 是最常用的 Coroutine builders栗柒,不阻塞當(dāng)前線程礁扮,在后臺(tái)創(chuàng)建一個(gè)新協(xié)程,也可以指定協(xié)程調(diào)度器瞬沦,例如在 Android 中常用的GlobalScope.launch(Dispatchers.Main) {}太伊。

fun postItem(item: Item) {
    GlobalScope.launch(Dispatchers.Main) { // 在 UI 線程創(chuàng)建一個(gè)新協(xié)程
        val token = requestToken()
        val post = createPost(token, item)
        processPost(post)
    }
}
3.4.2 runBlocking {}

runBlocking {}是創(chuàng)建一個(gè)新的協(xié)程同時(shí)阻塞當(dāng)前線程,直到協(xié)程結(jié)束逛钻。這個(gè)不應(yīng)該在協(xié)程中使用僚焦,主要是為main函數(shù)和測(cè)試設(shè)計(jì)的。

fun main(args: Array<String>) = runBlocking { // start main coroutine
    launch { // launch new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main coroutine continues here immediately
    delay(2000L)      // delaying for 2 seconds to keep JVM alive
}

class MyTest {
    @Test
    fun testMySuspendingFunction() = runBlocking {
        // here we can use suspending functions using any assertion style that we like
    }
}
3.4.3 withContext {}

withContext {}不會(huì)創(chuàng)建新的協(xié)程曙痘,在指定協(xié)程上運(yùn)行掛起代碼塊芳悲,并掛起該協(xié)程直至代碼塊運(yùn)行完成立肘。

3.4.4 async {}

CoroutineScope.async {}可以實(shí)現(xiàn)與 launch builder 一樣的效果,在后臺(tái)創(chuàng)建一個(gè)新協(xié)程名扛,唯一的區(qū)別是它有返回值谅年,因?yàn)?code>CoroutineScope.async {}返回的是 Deferred 類型。

fun main(args: Array<String>) = runBlocking { // start main coroutine
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }  // start async one coroutine without suspend main coroutine
        val two = async { doSomethingUsefulTwo() }  // start async two coroutine without suspend main coroutine
        println("The answer is ${one.await() + two.await()}") // suspend main coroutine for waiting two async coroutines to finish
    }
    println("Completed in $time ms")
}

獲取CoroutineScope.async {}的返回值需要通過await()函數(shù)肮韧,它也是是個(gè)掛起函數(shù)融蹂,調(diào)用時(shí)會(huì)掛起當(dāng)前協(xié)程直到 async 中代碼執(zhí)行完并返回某個(gè)值。

4. 小結(jié)

Kotlin 協(xié)程可以極大地簡(jiǎn)化異步編程弄企,雖然剛開始接觸的時(shí)候?qū)W習(xí)比較吃力超燃,但是接觸過一段時(shí)間相信絕對(duì)會(huì)愛上它。建議大家也去瀏覽下面推薦的資料拘领,可以更快地了解協(xié)程的大概意乓。

推薦閱讀:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市院究,隨后出現(xiàn)的幾起案子洽瞬,更是在濱河造成了極大的恐慌,老刑警劉巖业汰,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伙窃,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡样漆,警方通過查閱死者的電腦和手機(jī)为障,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來放祟,“玉大人鳍怨,你說我怎么就攤上這事」蛲祝” “怎么了鞋喇?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)眉撵。 經(jīng)常有香客問我侦香,道長(zhǎng),這世上最難降的妖魔是什么纽疟? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任罐韩,我火速辦了婚禮,結(jié)果婚禮上污朽,老公的妹妹穿的比我還像新娘散吵。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布矾睦。 她就那樣靜靜地躺著晦款,像睡著了一般。 火紅的嫁衣襯著肌膚如雪顷锰。 梳的紋絲不亂的頭發(fā)上柬赐,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音官紫,去河邊找鬼肛宋。 笑死,一個(gè)胖子當(dāng)著我的面吹牛束世,可吹牛的內(nèi)容都是我干的酝陈。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼毁涉,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼沉帮!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起贫堰,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤穆壕,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后其屏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體喇勋,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年偎行,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了川背。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蛤袒,死狀恐怖熄云,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情妙真,我是刑警寧澤缴允,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站珍德,受9級(jí)特大地震影響癌椿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜菱阵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望缩功。 院中可真熱鬧晴及,春花似錦、人聲如沸嫡锌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至蛛倦,卻和暖如春歌懒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背溯壶。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工及皂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人且改。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓验烧,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親又跛。 傳聞我的和親對(duì)象是個(gè)殘疾皇子碍拆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容