開始使用Kotlin協程

本文主要介紹協程的用法, 以及使用協程能帶來什么好處. 另外, 也會粗略提一下協程的大致原理.
本文的意義可能僅僅是讓你了解一下協程, 并愿意開始使用它.
如果想徹底理解協程, 請查看官方文檔, 官方文檔鏈接將在文章的結尾給出.

如果你以前在別的語言里學習過協程, 如Python的yield, 那請你先忘記它們, 畢竟還是有些區別, 等你弄懂了Kotlin的協程, 再去作對比, 否則, 可能會有一些先入為主的思路來阻礙你理解, 我就吃過這個虧.

初識協程:

首先我們來瞄一眼協程是長啥樣的, 以下引用(copy)了官網的一個例子:

fun main(args: Array<String>) {
    launch(CommonPool) {
        delay(1000L) 
        println("World!") 
    }
    println("Hello,")
    Thread.sleep(2000L)
}

/* 
運行結果: ("Hello,"會立即被打印, 1000毫秒之后, "World!"會被打印)
Hello, 
World!
*/

姑且不管里面具體的細節, 上面代碼大體的運行流程是這樣的:

A. 主流程:

  1. 調用系統的launch方法啟動了一個協程, 跟隨的大括號可以看做是協程體.
    (其中的CommonPool暫且理解成線程池, 指定了協程在哪里運行)
  2. 打印出"Hello,"
  3. 主線程sleep兩秒
    (這里的sleep只是保持進程存活, 目的是為了等待協程執行完)

B. 協程流程:

  1. 協程延時1秒
  2. 打印出"World!"

解釋一下delay方法:
在協程里delay方法作用等同于線程里的sleep, 都是休息一段時間, 但不同的是delay不會阻塞當前線程, 而像是設置了一個鬧鐘, 在鬧鐘未響之前, 運行該協程的線程可以被安排做了別的事情, 當鬧鐘響起時, 協程就會恢復運行.

協程啟動后還可以取消
launch方法有一個返回值, 類型是Job, Job有一個cancel方法, 調用cancel方法可以取消協程, 看一個數羊的例子:

fun main(args: Array<String>) {
    val job = launch(CommonPool) {
        var i = 1
        while(true) {
            println("$i little sheep")
            ++i
            delay(500L)  // 每半秒數一只, 一秒可以輸兩只
        }
    }

    Thread.sleep(1000L)  // 在主線程睡眠期間, 協程里已經數了兩只羊
    job.cancel()  // 協程才數了兩只羊, 就被取消了
    Thread.sleep(1000L)
    println("main process finished.")
}

運行結果是:

1 little sheep
2 little sheep
main process finished.

如果不調用cancel, 可以數到4只羊.

協程的核心是suspend方法, 下面先講解一下suspend方法, 之后再繼續別的話題.

理解suspend方法:

suspend方法是協程的核心, 理解suspend方法是使用和理解協程的關鍵.
(suspend lambda和suspend方法差不多, 只是沒有名字, 不再單獨介紹了)

suspend方法的語法很簡單, 只是比普通方法只是多了個suspend關鍵字:

suspend fun foo(): ReturnType {
    // ...
}

suspend方法只能在協程里面調用, 不能在協程外面調用.
suspend方法本質上, 與普通方法有較大的區別, suspend方法的本質是異步返回(注意: 不是異步回調). 后面我們會解釋這句話的含義.

現在, 我們先來看一個異步回調的例子:

fun main(...) {
  requestDataAsync {
    println("data is $it")
  }
  Thead.sleep(10000L)  // 這個sleep只是為了保活進程
}

fun requestDataAsync(callback: (String)->Unit) {
    Thread() {
        // do something need lots of times.
        // ...
        callback(data)
    }.start()
}

邏輯很簡單, 就是通過異步的方法拉一個數據, 然后使用這個數據, 按照以往的編程方式, 若要接受異步回來的數據, 唯有使用callback.
但是假如使用協程, 可以不使用callback, 而是直接把這個數據"return"回來, 調用者不使用callback接受數據, 而是像調用同步方法一樣接受返回值. 如果上述功能改用協程, 將會是:

fun main(...) {
    launch(Unconfined) {  // 請重點關注協程里是如何獲取異步數據的
        val data = requestDataAsync()  // 異步回來的數據, 像同步一樣return了
        println("data is $it")
    }

    Thead.sleep(10000L) // 請不要關注這個sleep
}

suspend fun requestDataAsync() { // 請注意方法前多了一個suspend關鍵字
    return async(CommonPool) { // 先不要管這個async方法, 后面解釋
        // do something need lots of times.
        // ...
        data  // return data, lambda里的return要省略
    }.await()
}

這里, 我們首先將requestDataAsync轉成了一個suspend方法, 其原型的變化是:

  1. 在前加了個suspend關鍵字.
  2. 去除了原來的callback參數.

這里先不去深究這個方法的新實現, 后面會專門解釋.
這里需要關注的點是: 在協程里面, 調用suspend方法, 異步的數據像同步一樣般return了.
這是怎么做到的呢?
當程序執行到requestDataAsync內部時, 通過async啟動了另外一個新的子協程去拉取數據, 啟動這個新的子協程后, 當前的父協程就掛起了, 此時requestDataAsync還沒有返回.
子協程一直在后臺跑, 過了一段時間, 子協程把數據拉回來之后, 會恢復它的父協程, 父協程繼續執行, requestDataAsync就把數據返回了.

為了加深理解, 我們來對比一下另一個例子: 不使用協程, 將異步方法也可以轉成同步的方法(在單元測試里, 我們經常這么做):

fun main(...) {
    val data = async2Sync()  // 數據是同步返回了, 但是線程也阻塞了
    println("data is $it")
    // Thead.sleep(10000L)  // 這一句在這里毫無意義了, 注釋掉
}

private var data = ""
private fun async2Sync(): String {
    val obj = Object() // 隨便創建一個對象當成鎖使用
    requestDataAsync { data ->
        this.data = data  // 暫存data
        synchronized(locker) {
            obj.notifyAll() // 通知所有的等待者
        }
    }
    obj.wait() // 阻塞等待
    return this.data
}

fun requestDataAsync(callback: (String)->Unit) {
    // ...普通的異步方法
}

注意對比上一個協程的例子, 這樣做表面上跟它是一樣的, 但是這里main方法會阻塞的等待async2Sync()方法完成. 同樣是等待, 協程就不會阻塞當前線程, 而是自己主動放棄執行權, 相當于遣散當前線程, 讓它去干別的事情去.

為了更好的理解這個"遣散"的含義, 我們再來看一個例子:

fun main(args: Array<String>) {
    // 1. 程序開始
    println("${Thread.currentThread().name}: 1");  

    // 2. 啟動一個協程, 并立即啟動
    launch(Unconfined) { // Unconfined意思是在當前線程(主線程)運行協程
        // 3. 本協程在主線程上直接開始執行了第一步
        println("${Thread.currentThread().name}: 2");  

        /* 4. 本協程的第二步調用了一個suspend方法, 調用之后, 
         * 本協程就放棄執行權, 遣散運行我的線程(主線程)請干別的去.
         * 
         * delay被調用的時候, 在內部創建了一個計時器, 并設了個callback.
         * 1秒后計時器到期, 就會調用剛設置的callback.
         * 在callback里面, 會調用系統的接口來恢復協程. 
         * 協程在計時器線程上恢復執行了. (不是主線程, 跟Unconfined有關)
         */
        delay(1000L)  // 過1秒后, 計時器線程會resume協程

        // 7. 計時器線程恢復了協程, 
        println("${Thread.currentThread().name}: 4")
    }

    // 5. 剛那個的協程不要我(主線程)干活了, 所以我繼續之前的執行
    println("${Thread.currentThread().name}: 3");

    // 6. 我(主線程)睡2秒鐘
    Thread.sleep(2000L)

    // 8. 我(主線程)睡完后繼續執行
    println("${Thread.currentThread().name}: 5");
}

運行結果:

main: 1
main: 2
main: 3
kotlinx.coroutines.ScheduledExecutor: 4
main: 5

上述代碼的注釋詳細的列出了程序運行流程, 看完之后, 應該就能明白 "遣散" 和 "放棄執行權" 的含義了.

Unconfined的含義是不給協程指定運行的線程, 逮到誰就使用誰, 啟動它的線程直接執行它, 但被掛起后, 會由恢復它的線程繼續執行, 如果一個協程會被掛起多次, 那么每次被恢復后, 都可能被不同線程繼續執行.

現在再來回顧剛剛那句: suspend方法的本質就是異步返回.
含義就是將其拆成 "異步" + "返回":

  • 首先, 數據不是同步回來的(同步指的是立即返回), 而是異步回來的.
  • 其次, 接受數據不需要通過callback, 而是直接接收返回值.

調用suspend方法的詳細流程是:
在協程里, 如果調用了一個suspend方法, 協程就會掛起, 釋放自己的執行權, 但在協程掛起之前, suspend方法內部一般會啟動了另一個線程或協程, 我們暫且稱之為"分支執行流"吧, 它的目的是運算得到一個數據.
當suspend方法里的*分支執行流"完成后, 就會調用系統API重新恢復協程的執行, 同時會數據返回給協程(如果有的話).

__為什么不能再協程外面調用suspend方法? __
suspend方法只能在協程里面調用, 原因是只有在協程里, 才能遣散當前線程, 在協程外面, 不允許遣散, 反過來思考, 假如在協程外面也能遣散線程, 會怎么樣, 寫一個反例:

fun main(args: Array<String>) {
    requestDataSuspend(); 
    doSomethingNormal();
}
suspend fun requestDataSuspend() { 
    // ... 
}
fun doSomethingNormal() {
    // ...
}

requestDataSuspend是suspend方法, doSomethingNormal是正常方法, doSomethingNormal必須等到requestDataSuspend執行完才會開始, 后果main方法失去了并行的能力, 所有地方都失去了并行的能力, 這肯定不是我們要的, 所以需要約定只能在協程里才可以遣散線程, 放棄執行權, 于是suspend方法只能在協程里面調用.

概念解釋: Continuation 與 suspension point

----個人建議專有名詞別翻譯成中文, 否則很容易因為斷句錯誤而產生誤解

協程的執行其實是斷斷續續的: 執行一段, 掛起來, 再執行一段, 再掛起來, ...
每個掛起的地方是一個suspension point, 每一小段執行是一個Continuation.
協程的執行流被它的 "suspension point" 分割成了很多個 "Continuation" .
我們可以用一條畫了很多點的線段來表示:

協程的執行流分段

其中的Continuation 0比較特殊, 是從起點開始, 到第一個suspension point結束, 由于它的特殊性, 又被稱為Initial Continuation.

協程創建后, 并不總是立即執行, 要分是怎么創建的協程, 通過launch方法的第二個參數是一個枚舉類型CoroutineStart, 如果不填, 默認值是DEFAULT, 那么久協程創建后立即啟動, 如果傳入LAZY, 創建后就不會立即啟動, 直到調用Job的start方法才會啟動.

suspension point只是一個概念, 而Continuation在Kotlin里有一個對應interface, 關于這個interface后面再介紹.

封裝異步回調方法

在沒有協程的世界里, 通常異步的方法都需要接受一個callback用于發布運算結果.
在協程里, 所有接受callback的方法, 都可以轉成不需要callback的suspend方法.

上面的requestDataSuspend方法就是一個這樣的例子, 我們回過頭來再看一眼:

suspend fun requestDataSuspend() {
    return async(CommonPool) {
        // do something need lots of times.
        // ...
        data  // return data
    }.await()
}

其內部通過調用了async和await方法來實現(關于async和await我們后面再介紹), 這樣雖然實現功能沒問題, 但并不最合適的方式, 上面那樣做只是為了追求最簡短的實現, 合理的實現應該是調用suspendCoroutine方法, 大概是這樣:

suspend fun requestDataSuspend() {
    suspendCoroutine { cont ->
        // ... 細節暫時省略
    }
}
// 可簡寫成:
suspend fun requestDataSuspend() = suspendCoroutine { cont ->
    // ...
}

在完整實現之前, 需要先理解suspendCoroutine方法, 它是Kotlin標準庫里的一個方法, 原型如下:

suspend fun <T> suspendCoroutine(block: (Continuation<T>) -> Unit): T

后面我們討論Kotlin協程官方API的時候就會知道, 這是Kotlin標準庫里用于支持協程的底層API非常少(大多數API不在標準庫, 而是在應用層的擴展庫, 如上面的launch方法), 這是其中一個.
suspendCoroutine的作用就是將當前執行流掛起, 在適合的時機再將協程恢復執行, 我們可以看到他的參數是一個lambda, lambda的參數是一個Continuation, 我們剛剛其實已經提到過Continuation了, 它表示一段執行流, 這里就不做過多解釋了, 這個方法里的Continuation實例代表的執行流是從當前的suspension point開始, 到下一個suspension point結束, 當前的suspension point就是調用suspendCoroutine這一刻.
調用suspendCoroutine之后, 當前的執行流會掛起(調用suspendCoroutine的線程會遣散, 但不是整個進程都掛起, 不然誰做事呢), 然后開另一個執行流去做異步的事情, 等到異步的事情做完, 當前的執行流又會恢復, 下面看一下是如何恢復的.
suspendCoroutine的會自動捕獲當前的執行環境(如臨時變量, 參數等), 然后存放到一個Continuation中, 并且作為參數傳給它的lambda.
之前已經提到Continuation是標準庫里的一個interface, 它的原型是:

interface Continuation<in T> {
   val context: CoroutineContext // 暫時不管這個
   fun resume(value: T)
   fun resumeWithException(exception: Throwable)
}

它有兩個方法resumeresumeWithException:

  • 若調用resume就是正?;謴?/li>
  • 調用resumeWithException就是異?;謴?/li>

現在來完善一下剛剛的例子:

suspend fun requestDataSuspend() = suspendCoroutine { cont ->
    requestDataFromServer { data -> // 普通方法還是通過callback接受數據
        if (data != null) {
            cont.resume(data)
        } else {
            cont.resumeWithException(MyException())
        }
    }
}

/** 普通的異步回調方法 */
fun requestDataFromServer(callback: (String)->Unit) {
    // ... get data from server, it will call back when finished.
}

邏輯很簡單, 如果data有效就正?;謴? 否則異?;謴?
但這里需要注意的是: 傳給resume的參數會變成suspendCoroutine的返回值, 進而成為了requestDataSuspend方法的返回值.
這個地方太神奇了, Kotlin是如何做到的呢?, 估計短時間也難以理解, 先記住吧.
suspendCoroutine有個特點:

suspendCoroutine { cont ->
    // 如果本lambda里返回前, cont的resume和resumeWithException都沒有調用
    // 那么當前執行流就會掛起, 并且掛起的時機是在suspendCoroutine之前
    // 就是在suspendCoroutine內部return之前就掛起了
    
    // 如果本lambda里返回前, 調用了cont的resume或resumeWithException
    // 那么當前執行流不會掛起, suspendCoroutine直接返回了, 
    // 若調用的是resume, suspendCoroutine就會像普通方法一樣返回一個值
    // 若調用的是resumeWithException, suspendCoroutine會拋出一個異常
    // 外面可以通過try-catch來捕獲這個異常
}

回過頭來看一下, 剛剛的實現有調用resume方法嗎, 我們把它折疊一下:

suspend fun requestDataSuspend() = suspendCoroutine { cont ->
    requestDataFromServer { ... }
}

清晰了吧, 沒有調用, 所以suspendCoroutine還沒有返回之前就掛起了, 但是掛起之前lambda執行完了, lambda里調用了requestDataFromServer, requestDataFromServer里啟動了真正做事的流程(異步執行的), 而suspendCoroutine則在掛起等待.
等到requestDataFromServer完成工作, 就會調用傳入的callback, 而這個callback里調用了cont.resume(data), 外層的協程就恢復了, 隨后suspendCoroutine就會返回, 返回值就是data.

大家一定很好奇Kotlin內部是如何實現的, 要像徹底了解其中的奧妙, 還是要看官方文檔和代碼, 這里只簡單介紹一下大致原理, 太細的我也不懂, 大家湊合著看一下, 看不懂也沒關系.
在Kotlin內部, 協程被實現成了一個狀態機, 狀態的個數就是suspension point的個數+1(初始狀態), 當前的狀態就是當前的suspension point, 當調用resume時, 就會執行下一個Continuation.

估計大家這個時候應該是似懂非懂, 其實作為使用者, 這已經夠了, 但是要深入研究, 還是靠自己研究代碼.

async/await模式:

我們前面多次使用了launch方法, 它的作用是創建協程并立即啟動, 但是有一個問題, 就是通過launch方法創建的協程都沒辦法攜帶返回值. async之前也出現過, 但一直沒有詳細介紹.

async方法作用和launch方法基本一樣, 創建一個協程并立即啟動, 但是async創建的協程可以攜帶返回值.
launch方法的返回值類型是Job, async方法的返回值類型是Deferred, 是Job的子類, Deferred里有個await方法, 調用它可得到協程的返回值.

async/await是一種常用的模式, async的含義是啟動一個異步操作, await的含義是等待這個異步操作結果.
是誰要等它啊, 在傳統的不使用協程的代碼里, 是線程在等(線程不干別的事, 就在那里傻等). 在協程里不是線程在等, 而且是執行流在等, 當前的流程掛起(底下的線程會被遣散去干別的事), 等到有了運算結果, 流程才繼續運行.
所以我們又可以順便得出一個結論: 在協程里執行流是線性的, 其中的步驟無論是同步的還是異步的, 后面的步驟都會等前面的步驟完成.
我們可以通過async起多個任務, 他們會同時運行, 我們之前使用的async姿勢不是很正常, 下面看一下使用async正常的姿勢:

fun main(...) {
    launch(Unconfined) {
        // 任務1會立即啟動, 并且會在別的線程上并行執行
        val deferred1 = async { requestDataAsync1() }
        
        // 上一個步驟只是啟動了任務1, 并不會掛起當前協程
        // 所以任務2也會立即啟動, 也會在別的線程上并行執行
        val deferred2 = async { requestDataAsync2() }

        // 先等待任務1結束(等了約1000ms), 
        // 然后等待任務2, 由于它和任務1幾乎同時啟動的, 所以也很快完成了
        println("data1=$deferred2.await(), data2=$deferred2.await()")
    }

    Thead.sleep(10000L) // 繼續無視這個sleep
}

suspend fun requestDataAsync1(): String {
    delay(1000L)
    return "data1"    
}
suspend fun requestDataAsync2(): String {
    delay(1000L)
    return "data2"    
}

運行結果很簡單, 不用說了, 但是協程總耗時是多少呢, 約1000ms, 不是2000ms, 因為兩個任務是并行運行的.
有一個問題: 假如任務2先于任務1完成, 結果是怎樣的呢?
答案是: 任務2的結果會先保存在deferred2里, 當調用deferred2.await()時, 會立即返回, 不會引起協程掛起, 因為deferred2已經準備好了.
所以, suspend方法并不總是引起協程掛起, 只有其內部的數據未準備好時才會.

需要注意的是: await是suspend方法, 但async不是, 所以它才可以在協程外面調用, async只是啟動了協程, async本身不會引起協程掛起, 傳給async的lambda(也就是協程體)才可能引起協程掛起.

async/await模式在別的語言里, 被實現成了兩個關鍵字, 但在Kotlin里只是兩個很平常的方法.

Generators介紹:

學習Python的協程的時候, 最先學習的就是Generators, 它的作用就是通過計算產生序列, 而不用通過列表之類存儲機制. 以下通過Generators產生斐波那契序列:

// inferred type is Sequence<Int>
val fibonacci = buildSequence {
    yield(1) // first Fibonacci number
    var cur = 1
    var next = 1
    while (true) {
        yield(next) // next Fibonacci number
        val tmp = cur + next
        cur = next
        next = tmp
    }
}

fun main(...) {
    launch(Unconfined) {  // 請重點關注協程里是如何獲取異步數據的
        fibonacci.take(10).forEach { print("$it, ") }
    }

    Thead.sleep(10000L) // 請不要關注這個sleep
}

// will print 1, 1, 2, 3, 5, 8, 13, 21, 34, 55,

我覺得這個沒什么好解釋的, yield是一個suspend方法, 放棄執行權, 并將數據返回.
根據前面的知識, 我們可以推斷出, yield內部肯定最終會調用到Continuation的resume方法.

yield在別的語言, 一般是一個關鍵字, Kotlin中也是一個方法.

yield是標準庫里的API, 大多數情況我們不需要直接調用這個方法, 使用kotlinx.coroutines里面的Channelproduce方法更加方法. 具體可以參考這里:
https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md#building-channel-producers
序列的產生跟RX其實有點像, 但也是區別的, 具體可以參考這里:
https://github.com/Kotlin/kotlinx.coroutines/blob/master/reactive/coroutines-guide-reactive.md

目前沒有發現特別需要使用Generators的場景, 所以這里不做太多討論.

協程API說明:

Kotlin的開發者對協程的實現比較獨特, 語言機制本身只增加了極少的關鍵字, 標準庫也只有極少的API, 但這并不代表功能少, 根據Kotlin的設計, 很多功能型API都放到了更上層的應用庫里去實現.
只將少量核心的機制才放到語言本身和標準庫上. 這樣做不僅使得語言更簡單, 而且靈活性更強.

Kotlin官方對協程提供的三種級別的能力支持, 分別是: 最底層的語言層, 中間層標準庫(kotlin-stdlib), 以及最上層應用層(kotlinx.coroutines).

應用層:
這一層是我們的程序直接調用的層, 提供一些常用的實現方法, 如launch方法, async方法等, 它的實現在kotlinx.coroutines里面.

標準庫:
標準庫僅僅提供了少量創建協程的方法, 位于:
kotlin.coroutines.experimental:
-- createCoroutine()
-- startCoroutine()
-- suspendCoroutine()

到目前為止, 我們直接使用到的只有suspendCoroutine方法.
launch和async方法的實現里最終調用了startCoroutine方法.
Generators里的buildSequence方法, 最終會調用createCoroutine來實現.

語言層:
語言本身主要提供了對suspend關鍵字的支持, Kotlin編譯器會對suspend修飾的方法或lambda特殊處理, 生成一些中間類和邏輯代碼.

我們平常用到的基本都是應用層的接口, 應用層提供了很多非常核心功能, 這些功能在其他語言里大多是通過關鍵字來實現的, 而在Kotlin里, 這些都是實現成了方法.

總結


協程是什么:

看了這么多例子, 我們現在可以總結一下協程是什么, 協程到底是什么, 很難給出具體的定義, 就算能給出具體定義, 也會非常抽象難以理解的.
另一方面, 協程可以說是編譯器的能力, 因為協程并不需要操作系統和硬件的支持(線程需要), 是編譯器為了讓開發者寫代碼更簡單方便, 提供了一些關鍵字, 并在內部自動生成了一些支持型代碼(可能是字節碼).

以下我個人的總結:

  • 首先, 協程是一片包含特定邏輯的代碼塊, 這個代碼塊可以調度到不同的線程上執行;
  • 其次, 協程一種環境, 在這種環境里, 方法可以被等待執行, 有了運算結果之后才返回, 在等待期間, 承載協程的線程資源可以被別的地方使用.
  • 第三, 協程是一個獨立于運行流程的邏輯流程, 協程里面的步驟, 無論是同步的還是異步的, 都是線性(從前到后依次完成的).

協程和線程區別與關系:

線程和協程的目的本質上存在差異:

  • 線程的目的是提高CPU資源使用率, 使多個任務得以并行的運行, 所以線程是為了服務于機器的.
  • 協程的目的是為了讓多個任務之間更好的協作, 主要體現在代碼邏輯上, 所以協程是為了服務于人的, 寫代碼的人. (也有可能結果會能提升資源的利用率, 但并不是原始目的)

在調度上, 協程跟線程也不同:

  • 線程的調度是系統完成的, 一般是搶占式的, 根據優先級來分配, 是空分復用.
  • 協程的調度是開發者根據程序邏輯指定好的, 在不同的時期把資源合理的分配給不同的任務, 是時分復用的.

作用上的不同:

  • 協程確保了代碼邏輯是順序的, 不管同步操作要是異步操作, 前一個完成, 后一個才會開始.
  • 線程可以被調度到CPU上執行, 這樣代碼才能真正運行起來.

協程與線程的關系:
協程并不是取代線程, 而且抽象于線程之上, 線程是被分割的CPU資源, 協程是組織好的代碼流程, 協程需要線程來承載運行, 線程是協程的資源, 但協程不會直接使用線程, 協程直接利用的是執行器(Interceptor), 執行器可以關聯任意線程或線程池, 可以使當前線程, UI線程, 或新建新程. 可總結如下:

  1. 線程是協程的資源.
  2. 協程通過Interceptor來間接使用線程這個資源.

結語:

如果需要經常使用協程, 建議抽時間看一下官方文檔.
最后, 感謝大家的閱讀, 希望本文對你有所幫助 !


官方英文文檔連接:

  1. 官方的協程簡介
  2. 完整版的使用指南
  3. 當前的實現方案詳解

_
第1個頁面是是官方指南的子頁面, 第2個和第3個分別是兩個GitHub項目里面的markdown文檔, 他們所在的工程還包含其他文檔, 有需要可以自行瀏覽.
另外, 提個建議: 如果看著看著卡殼了, 可以跳過或查閱另外幾個文檔, 以后再回過來看, 別的文檔有可能會用別的方式或別的例子來描述了同一個東西)
_

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

推薦閱讀更多精彩內容