Kotlin Coroutines(協程) 完全解析(一),協程簡介

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

Kotlin Coroutines(協程) 完全解析(一),協程簡介

Kotlin Coroutines(協程) 完全解析(二),深入理解協程的掛起、恢復與調度

Kotlin Coroutines(協程) 完全解析(三),封裝異步回調、協程間關系及協程的取消

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

Kotlin Coroutines(協程) 完全解析(五),協程的并發

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

Kotlin 中引入 Coroutine(協程) 的概念,可以幫助編寫異步代碼,目前還是試驗性的。國內詳細介紹協程的資料比較少,所以我打算寫 Kotlin Coroutines(協程) 完全解析的系列文章,希望可以幫助大家更好地理解協程。這是系列文章的第一篇,簡單介紹協程的特點和一些基本概念。協程主要的目的是簡化異步編程,那么先從為什么需要協程來編寫異步代碼開始。

Kotlin Coroutine 終于正式發布了,所以我跟進最新的正式版更新了本文相關內容

1. 為什么需要協程?

異步編程中最為常見的場景是:在后臺線程執行一個復雜任務,下一個任務依賴于上一個任務的執行結果,所以必須等待上一個任務執行完成后才能開始執行。看下面代碼中的三個函數,后兩個函數都依賴于前一個函數的執行結果。

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
}

三個函數中的操作都是耗時操作,因此不能直接在 UI 線程中運行,而且后兩個函數都依賴于前一個函數的執行結果,三個任務不能并行運行,該如何解決這個問題呢?

1.1 回調

常見的做法是使用回調,把之后需要執行的任務封裝為回調。

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)
        }
    }
}

回調在只有兩個任務的場景是非常簡單實用的,很多網絡請求框架的 onSuccess Listener 就是使用回調,但是在三個以上任務的場景中就會出現多層回調嵌套的問題,而且不方便處理異常。

1.2 Future

Java 8 引入的 CompletableFuture 可以將多個任務串聯起來,可以避免多層嵌套的問題。

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
            }
}

上面代碼中使用連接符串聯起三個任務,最后的exceptionally方法還可以統一處理異常情況,但是只能在 Java 8 以上才能使用。

1.3 Rx 編程

CompletableFuture 的方式有點類似 Rx 系列的鏈式調用,這也是目前大多數推薦的做法。

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 豐富的操作符、簡便的線程調度、異常處理使得大多數人滿意,我也如此,但是還沒有更簡潔易讀的寫法呢?

1.4 協程

下面是使用 Kotlin 協程的代碼:

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

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

使用協程后的代碼非常簡潔,以順序的方式書寫異步代碼,不會阻塞當前 UI 線程,錯誤處理也和平常代碼一樣簡單。

2. 協程是什么

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 協程的定義

先看官方文檔的描述:

協程通過將復雜性放入庫來簡化異步編程。程序的邏輯可以在協程中順序地表達,而底層庫會為我們解決其異步性。該庫可以將用戶代碼的相關部分包裝為回調、訂閱相關事件、在不同線程(甚至不同機器)上調度執行,而代碼則保持如同順序執行一樣簡單。

協程的開發人員 Roman Elizarov 是這樣描述協程的:協程就像非常輕量級的線程。線程是由系統調度的,線程切換或線程阻塞的開銷都比較大。而協程依賴于線程,但是協程掛起時不需要阻塞線程,幾乎是無代價的,協程是由開發者控制的。所以協程也像用戶態的線程,非常輕量級,一個線程中可以創建任意個協程。

總而言之:協程可以簡化異步編程,可以順序地表達程序,協程也提供了一種避免阻塞線程并用更廉價、更可控的操作替代線程阻塞的方法 -- 協程掛起。

3. 協程的基本概念

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

3.1 掛起函數

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

requestTokencreatePost函數前面有suspend修飾符標記,這表示兩個函數都是掛起函數。掛起函數能夠以與普通函數相同的方式獲取參數和返回值,但是調用函數可能掛起協程(如果相關調用的結果已經可用,庫可以決定繼續進行而不掛起),掛起函數掛起協程時,不會阻塞協程所在的線程。掛起函數執行完成后會恢復協程,后面的代碼才會繼續執行。但是掛起函數只能在協程中或其他掛起函數中調用。事實上,要啟動協程,至少要有一個掛起函數,它通常是一個掛起 lambda 表達式。所以suspend修飾符可以標記普通函數、擴展函數和 lambda 表達式。

掛起函數只能在協程中或其他掛起函數中調用,上面例子中launch函數就創建了一個協程。

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

launch函數:

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

從上面函數定義中可以看到協程的一些重要的概念:CoroutineContext、CoroutineDispatcher、Job,下面來一一介紹這些概念。

3.1 CoroutineScope 和 CoroutineContext

CoroutineScope,可以理解為協程本身,包含了 CoroutineContext。

CoroutineContext,協程上下文,是一些元素的集合,主要包括 Job 和 CoroutineDispatcher 元素,可以代表一個協程的場景。

EmptyCoroutineContext 表示一個空的協程上下文。

3.2 CoroutineDispatcher

CoroutineDispatcher,協程調度器,決定協程所在的線程或線程池。它可以指定協程運行于特定的一個線程、一個線程池或者不指定任何線程(這樣協程就會運行于當前線程)。coroutines-core中 CoroutineDispatcher 有三種標準實現Dispatchers.DefaultDispatchers.IODispatchers.MainDispatchers.Unconfined,Unconfined 就是不指定線程。

launch函數定義如果不指定CoroutineDispatcher或者沒有其他的ContinuationInterceptor,默認的協程調度器就是Dispatchers.DefaultDefault是一個協程調度器,其指定的線程為共有的線程池,線程數量至少為 2 最大與 CPU 數相同。

3.3 Job & Deferred

Job,任務,封裝了協程中需要執行的代碼邏輯。Job 可以取消并且有簡單生命周期,它有三種狀態:

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 完成時是沒有返回值的,如果需要返回值的話,應該使用 Deferred,它是 Job 的子類public interface Deferred<out T> : Job

3.4 Coroutine builders

CoroutineScope.launch函數屬于協程構建器 Coroutine builders,Kotlin 中還有其他幾種 Builders,負責創建協程。

3.4.1 CoroutineScope.launch {}

CoroutineScope.launch {} 是最常用的 Coroutine builders,不阻塞當前線程,在后臺創建一個新協程,也可以指定協程調度器,例如在 Android 中常用的GlobalScope.launch(Dispatchers.Main) {}

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

runBlocking {}是創建一個新的協程同時阻塞當前線程,直到協程結束。這個不應該在協程中使用,主要是為main函數和測試設計的。

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 {}不會創建新的協程,在指定協程上運行掛起代碼塊,并掛起該協程直至代碼塊運行完成。

3.4.4 async {}

CoroutineScope.async {}可以實現與 launch builder 一樣的效果,在后臺創建一個新協程,唯一的區別是它有返回值,因為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()函數,它也是是個掛起函數,調用時會掛起當前協程直到 async 中代碼執行完并返回某個值。

4. 小結

Kotlin 協程可以極大地簡化異步編程,雖然剛開始接觸的時候學習比較吃力,但是接觸過一段時間相信絕對會愛上它。建議大家也去瀏覽下面推薦的資料,可以更快地了解協程的大概。

推薦閱讀:

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

推薦閱讀更多精彩內容