Kotlin 協(xié)程入門(mén)

本文主要介紹協(xié)程長(zhǎng)什么樣子, 協(xié)程是什么東西, 協(xié)程掛起的實(shí)現(xiàn)原理以及整理了協(xié)程學(xué)習(xí)的資料.

協(xié)程 HelloWorld

協(xié)程在官方指南中被稱為一種輕量級(jí)的線程, 所以在介紹協(xié)程是什么東西之前, 這里通過(guò)幾個(gè)與線程對(duì)比的小例子初步認(rèn)識(shí)協(xié)程.

啟動(dòng)線程與啟動(dòng)協(xié)程

/* Kotlin code - Example 1.1 */
// 創(chuàng)建一條新線程并輸出 Hello World.
thread {
    println("使用線程輸出 Hello World! Run in ${Thread.currentThread()}")
}

// 創(chuàng)建一個(gè)協(xié)程并使用協(xié)程輸出 Hello World.
GlobalScope.launch {
    println("使用協(xié)程輸出 Hello World! Run in ${Thread.currentThread()}")
}

/* output */
使用線程輸出 Hello World! Run in Thread[Thread-0,5,main]
使用協(xié)程輸出 Hello World! Run in Thread[DefaultDispatcher-worker-1,5,main]

上面的例子是一個(gè)簡(jiǎn)單的輸出 Hello World 的程序. 在這個(gè)例子中, 我們可以看到創(chuàng)建并啟動(dòng)一條協(xié)程和創(chuàng)建并啟動(dòng)一條線程的代碼幾乎一致, 唯一不同的就是創(chuàng)建線程調(diào)用的是 #thread 方法, 而創(chuàng)建協(xié)程調(diào)用的是 GlobalScope#launch 方法.

暫停線程與暫停協(xié)程

/* Kotlin code - Example 1.2 */
fun demoSleep() {
    // 創(chuàng)建并運(yùn)行一條線程, 在線程中使用 Thread#sleep 暫停線程運(yùn)行 100ms.
    thread {
        val useTime = measureTimeMillis {
            println("線程啟動(dòng)")
            println("線程 sleep 開(kāi)始")
            Thread.sleep(100L)
            println("線程結(jié)束")
        }
        println("線程用時(shí)為 $useTime ms")
    }
}

fun demoDelay() {
    // 創(chuàng)建并運(yùn)行一條協(xié)程, 在協(xié)程中使用 #delay 暫停協(xié)程運(yùn)行 100 ms.
    GlobalScope.launch {
        val useTime = measureTimeMillis {
            println("協(xié)程啟動(dòng)")
            println("協(xié)程 delay 開(kāi)始")
            delay(100L)
            println("協(xié)程結(jié)束")
        }
        println("協(xié)程用時(shí)為 $useTime ms")
    }
}

/* output */
線程啟動(dòng)
線程 sleep 開(kāi)始
線程結(jié)束
線程用時(shí)為 102 ms

協(xié)程啟動(dòng)
協(xié)程 delay 開(kāi)始
協(xié)程結(jié)束
協(xié)程用時(shí)為 106 ms

上面例子展示了暫停線程和暫停協(xié)程的方法. 我們可以使用 Thread#sleep 方法暫停一條線程, 而暫停一條協(xié)程, 只需要把 Thread#sleep 直接替換成 #delay 就可以了.

等待線程執(zhí)行結(jié)束與等待協(xié)程執(zhí)行結(jié)束

/* Kotlin code - Example 1.3 */
/**
 * 線程等待另外一個(gè)線程任務(wù)完成的方法
 */
private fun waitOtherJobThread() {
    // 啟動(dòng)線程 A
    thread {
        println("線程 A: 啟動(dòng)")

        // 隨便定義一個(gè)變量用于阻塞線程 A
        val waitThreadB = Object()

        // 啟動(dòng)線程 B
        val threadB = thread {
            println("線程 B: 啟動(dòng)")
            println("線程 B: 開(kāi)始執(zhí)行任務(wù)")
            for (i in 0..99) {
                Math.E * Math.PI
            }
            println("線程 B: 結(jié)束")
        }

        // 線程 A 等待線程 B 完成任務(wù)
        println("線程 A: 等待線程 B 完成")
        threadB.join()
        println("線程 A: 等待結(jié)束")

        println("線程 A: 結(jié)束")
    }
}

/**
 * 協(xié)程等待另外一個(gè)協(xié)程任務(wù)完成的方法
 */
private fun waitOtherJobCoroutine() {
    // 啟動(dòng)協(xié)程 A
    GlobalScope.launch {
        println("協(xié)程 A: 啟動(dòng)")

        // 啟動(dòng)協(xié)程 B
        val coroutineB = GlobalScope.launch {
            println("協(xié)程 B: 啟動(dòng)")
            println("協(xié)程 B: 開(kāi)始執(zhí)行任務(wù)")
            for (i in 0..99) {
                Math.E * Math.PI
            }
            println("協(xié)程 B: 結(jié)束")
        }

        // 協(xié)程 A 等待協(xié)程 B 完成
        println("協(xié)程 A: 等待協(xié)程 B 完成")
        coroutineB.join()
        println("協(xié)程 A: 等待結(jié)束")

        println("協(xié)程 A: 結(jié)束")
    }
}

/* output */
線程 A: 啟動(dòng)
線程 A: 等待線程 B 完成
線程 B: 啟動(dòng)
線程 B: 開(kāi)始執(zhí)行任務(wù)
線程 B: 結(jié)束
線程 A: 等待結(jié)束
線程 A: 結(jié)束

協(xié)程 A: 啟動(dòng)
協(xié)程 A: 等待協(xié)程 B 完成
協(xié)程 B: 啟動(dòng)
協(xié)程 B: 開(kāi)始執(zhí)行任務(wù)
協(xié)程 B: 結(jié)束
協(xié)程 A: 等待結(jié)束
協(xié)程 A: 結(jié)束

在上面的例子中, 創(chuàng)建了一條線程 A(協(xié)程 A), 然后在線程 A(協(xié)程 A)中再創(chuàng)建一條線程 B(協(xié)程 B), 接著使用 #join 方法使線程 A(協(xié)程 A)等待線程 B(協(xié)程 B)執(zhí)行結(jié)束. 我們可以清楚的看到等待線程和等待協(xié)程的代碼幾乎是一致的, 甚至連等待的方法都是 #join.

中斷線程與中斷協(xié)程

/* Kotlin code -  Example 1.4 線程的中斷與協(xié)程的中斷. */
private fun cancelThread() {
    val job1 = thread {
        println("線程: 啟動(dòng)")

        // 循環(huán)執(zhí)行 100 個(gè)耗時(shí)任務(wù).
        for (i in 0..99) {
            try {
                Thread.sleep(50L)
                println("線程: 正在執(zhí)行任務(wù) $i...")
            } catch (e: InterruptedException) {
                println("線程: 被中斷了")
                break
            }
        }

        println("線程: 結(jié)束")
    }

    // 延時(shí) 200ms 后中斷線程.
    Thread.sleep(200L)
    println("中斷線程!!!")
    job1.interrupt()
}

private fun cancelCoroutine() = runBlocking {
    val job1 = GlobalScope.launch {
        println("協(xié)程: 啟動(dòng)")

        // 循環(huán)執(zhí)行 100 個(gè)耗時(shí)任務(wù).
        for (i in 0..99) {
            try {
                delay(50L)
                println("協(xié)程: 正在執(zhí)行任務(wù) $i...")
            } catch (cancelException: CancellationException) {
                println("協(xié)程: 被中斷了")
                break
            }
        }

        println("協(xié)程: 結(jié)束")
    }

    // 延時(shí) 200ms 后中斷協(xié)程.
    delay(200L)
    println("中斷協(xié)程!!!")
    job1.cancel()
}

/* output */
線程: 啟動(dòng)
線程: 正在執(zhí)行任務(wù) 0...
線程: 正在執(zhí)行任務(wù) 1...
線程: 正在執(zhí)行任務(wù) 2...
中斷線程!!!
線程: 被中斷了
線程: 結(jié)束

協(xié)程: 啟動(dòng)
協(xié)程: 正在執(zhí)行任務(wù) 0...
協(xié)程: 正在執(zhí)行任務(wù) 1...
協(xié)程: 正在執(zhí)行任務(wù) 2...
中斷協(xié)程!!!
協(xié)程: 被中斷了
協(xié)程: 結(jié)束

在上面例子中, 可以看到中斷線程調(diào)用的方法是 #interrupt, 當(dāng)線程被中斷后會(huì)拋出 InterruptedException. 中斷協(xié)程的方法為 #cancel. 協(xié)程被中斷后會(huì)拋出 CancellationException.

通過(guò)上面的幾個(gè)小例子, 我們可以看到幾乎每一個(gè)線程的方法在協(xié)程中都有一個(gè)方法與之對(duì)應(yīng). 除了調(diào)用的方法名稱不一樣, 協(xié)程在使用上可以說(shuō)幾乎和線程沒(méi)有特別大的區(qū)別.


協(xié)程是什么?

可掛起的計(jì)算實(shí)例。 它在概念上類似于線程,在這個(gè)意義上,它需要一個(gè)代碼塊運(yùn)行,并具有類似的生命周期 —— 它可以被創(chuàng)建與啟動(dòng),但它不綁定到任何特定的線程。它可以在一個(gè)線程中掛起其執(zhí)行,并在另一個(gè)線程中恢復(fù)。而且,像 future 或 promise 那樣,它在完結(jié)時(shí)可能伴隨著某種結(jié)果(值或異常)。

上面這段話引用自 Kotlin 官方協(xié)程設(shè)計(jì)文檔中對(duì)協(xié)程的描述. 那么這段話應(yīng)該怎么理解呢? 首先, 協(xié)程需要一個(gè)計(jì)算實(shí)例. 類比與線程, 創(chuàng)建和啟動(dòng)線程同樣需要一個(gè)計(jì)算實(shí)例. 對(duì)于線程來(lái)說(shuō), 線程的計(jì)算實(shí)例是 Runnable, 我們需要把 Runnable 扔給線程才能在線程中完成計(jì)算任務(wù). 對(duì)于協(xié)程來(lái)說(shuō), 這個(gè)計(jì)算實(shí)例是 suspend 關(guān)鍵字修飾的方法或 lambda 表達(dá)式, 我們需要把這個(gè) suspend 關(guān)鍵字修飾的方法或 lambda 表達(dá)式扔給協(xié)程才能在協(xié)程中完成計(jì)算任務(wù). 接著, 除了需要一個(gè)計(jì)算實(shí)例之外, 協(xié)程中的這個(gè)計(jì)算實(shí)例還必須是可掛起的, 這也是協(xié)程和線程的區(qū)別. 那么可掛起是什么意思呢? 比如在上面暫停線程與暫停協(xié)程的例子中, 線程和協(xié)程同樣是等待 100ms, 在線程的實(shí)現(xiàn)方式中, 是通過(guò)調(diào)用 Thread#sleep 方法阻塞線程來(lái)實(shí)現(xiàn)的, 而在協(xié)程的實(shí)現(xiàn)中, 調(diào)用 #delay 實(shí)現(xiàn)的等待是不會(huì)阻塞任何線程的(協(xié)程也是運(yùn)行在某一條線程上的). 同樣是等待, 線程等待的實(shí)現(xiàn)方式會(huì)阻塞線程, 而協(xié)程等待的實(shí)現(xiàn)方式不會(huì)阻塞線程, 所以就把線程的等待稱之為阻塞, 把協(xié)程的等待稱之為掛起. 同樣的, 在上面等待線程執(zhí)行結(jié)束與等待協(xié)程執(zhí)行結(jié)束例子中, 線程調(diào)用 threadB#join 勢(shì)必會(huì)造成線程 A 的阻塞, 而在協(xié)程中, 調(diào)用 coroutineB#join 也能實(shí)現(xiàn)同樣的功能卻不會(huì)造成任何線程的阻塞.

協(xié)程掛起的實(shí)現(xiàn)原理

經(jīng)過(guò)上文的簡(jiǎn)單介紹, 我們知道了協(xié)程是什么, 協(xié)程和線程的區(qū)別是什么. 這里做個(gè)總結(jié), 協(xié)程是一個(gè)可掛起的計(jì)算實(shí)例, 和線程的區(qū)別就是協(xié)程的計(jì)算實(shí)例在執(zhí)行某些需要等待的任務(wù)時(shí)是可掛起的, 不阻塞線程的. 那么下面就開(kāi)始介紹 Kotlin 協(xié)程是怎么實(shí)現(xiàn)等待某些任務(wù)而不阻塞線程的.

/* Kotlin code - Example 2.1*/

/**
 * 自定義一個(gè) delay 掛起函數(shù). 功能和協(xié)程庫(kù)中的 [delay] 函數(shù)是一樣的.
 * 這里使用的是標(biāo)準(zhǔn)庫(kù)中定義掛起函數(shù)的方法.
 */
private suspend fun customDelay(delayInMillis: Long) = suspendCoroutine { complete: Continuation<Unit> ->
    // 創(chuàng)建一個(gè)可延時(shí)執(zhí)行任務(wù)的 Thread Executor.
    val executorService = Executors.newSingleThreadScheduledExecutor()

    // 延時(shí) delayInMillis ms 后調(diào)用 complete#resume 方法通知該任務(wù)已經(jīng)執(zhí)行完成了.
    executorService.schedule({
        complete.resume(Unit)
        executorService.shutdown()
    }, delayInMillis, TimeUnit.MILLISECONDS)
}

/**
 * suspend 函數(shù)可以類比于 Thread 中的 Runnable.
 * 同時(shí), suspend 函數(shù)還被看作掛起點(diǎn), 也就是說(shuō)運(yùn)行到這個(gè)函數(shù)的時(shí)候
 * 可能會(huì)被切換到其他線程當(dāng)中運(yùn)行.
 */
private suspend fun doSomething() {
    println("A")
    customDelay(10L)  // 掛起點(diǎn)

    println("B")
    customDelay(10L) // 掛起點(diǎn)

    println("C")
}

/**
 * Example 2.1 使用標(biāo)準(zhǔn)庫(kù)啟動(dòng)一個(gè)協(xié)程.
 */
fun main() {
    // 位于標(biāo)準(zhǔn)庫(kù)中的協(xié)程啟動(dòng)函數(shù)
    ::doSomething.startCoroutine(Continuation(EmptyCoroutineContext) {
        println(">>> doSomething Completed <<<")
    })

    // 防止進(jìn)程退出.
    Thread.sleep(1000L)
}

/* output */
A
B
C
>>> doSomething Completed <<<

在正式介紹協(xié)程掛起原理之前, 需要先簡(jiǎn)單介紹一下協(xié)程的幾個(gè)基本知識(shí)點(diǎn).

  1. 所有 suspend 修飾的函數(shù)或 lambda 表達(dá)式可以直接通過(guò) public fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>) 這個(gè)拓展方法創(chuàng)建并啟動(dòng)協(xié)程, 該方法在 suspend 修飾的計(jì)算實(shí)例(也就是前文提到的 suspend 修飾的函數(shù)或 lambda 表達(dá)式)完成計(jì)算后會(huì)回調(diào)參數(shù)的 #resumeWith 方法. 這個(gè)函數(shù)是最底層創(chuàng)建并啟動(dòng)協(xié)程的函數(shù), 所有封裝的協(xié)程構(gòu)建器最終都要通過(guò)這個(gè)方法來(lái)創(chuàng)建并啟動(dòng)一條協(xié)程. 在上面與線程對(duì)比的幾個(gè)小例子中使用到的 GlobalScope#launch 協(xié)程構(gòu)建器最終也會(huì)調(diào)用該方法來(lái)創(chuàng)建并啟動(dòng)協(xié)程. 這也是在協(xié)程是什么這一節(jié)提到協(xié)程需要的計(jì)算實(shí)例是 suspend 關(guān)鍵字修飾的方法或 lambda 表達(dá)式的原因.
  2. suspend 修飾的函數(shù)被稱為掛起函數(shù). 調(diào)用掛起函數(shù)可能會(huì)掛起計(jì)算實(shí)例, 所以調(diào)用掛起函數(shù)的地方也被稱為掛起點(diǎn). 在上面代碼示例中, #customDelay#doSomething 都是掛起函數(shù). 在 #doSomething 中調(diào)用 #customDelay 的地方被稱為掛起點(diǎn).
  3. public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T 這個(gè)函數(shù)的作用是把被編譯器隱藏的 Continuation 參數(shù)暴露出來(lái). 我們可以通過(guò)這個(gè)函數(shù)自定義自己的掛起函數(shù), 實(shí)現(xiàn)等待卻不阻塞線程的任務(wù).

在弄懂這幾個(gè)知識(shí)點(diǎn)之后, 上面的代碼就很容易知道是干什么的了. 上面的代碼實(shí)際上就是使用 #doSomething 創(chuàng)建并啟動(dòng)一條協(xié)程, 在 #doSomething 中依次輸出 "A", "B", "C". 執(zhí)行完 #doSomething 之后輸出 "doSomething Completed". 在執(zhí)行 #doSomething 的過(guò)程中會(huì)調(diào)用自定義的 #customDelay 方法掛起等待 10ms.

說(shuō)了這么多, 那么協(xié)程到底是如何實(shí)現(xiàn)等待而不阻塞線程的呢? 這里面的原理其實(shí)十分簡(jiǎn)單. 協(xié)程實(shí)現(xiàn)等待而不阻塞線程的方法就是通過(guò)回調(diào), 只不過(guò)這個(gè)回調(diào)是 Kotlin 編譯器實(shí)現(xiàn)的. 既然是編譯器實(shí)現(xiàn)的, 那么我們就需要反編譯一下這段代碼看看 Kotlin 編譯器到底做了什么黑科技的東西. 在 idea 中, Kotlin 編譯后的代碼可以通過(guò) Tools -> Kotlin -> show kotlin bytecode 這幾個(gè)步驟查看. 為了更加清晰的展示編譯器干了什么東西, 這里我就直接貼我整理過(guò)后的反編譯 Java 代碼了. 下面這段整理過(guò)的 Java 代碼和反編譯的代碼是等效的.

/* Java code - Example 2.2 */
public class StartCoroutineSimulation {

    /**
     * 一個(gè)可掛起的計(jì)算實(shí)例. 根據(jù)協(xié)程的定義, 這個(gè)接口的對(duì)象就是協(xié)程.
     */
    interface Continuation {

        /**
         * 喚醒被掛起的計(jì)算任務(wù), 繼續(xù)運(yùn)行.
         */
        void resume();
    }

    /**
     * 自定義一個(gè) delay 掛起函數(shù). 功能和協(xié)程庫(kù)中的 [delay] 函數(shù)是一樣的.
     * 這里使用的是標(biāo)準(zhǔn)庫(kù)中定義掛起函數(shù)的方法.
     */
    private static void customDelay(Continuation complete, long delayInMillis) {
        Continuation continuation = new Continuation() {
            @Override
            public void resume() {
                // 創(chuàng)建一個(gè)可延時(shí)執(zhí)行任務(wù)的 Thread Executor.
                ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();

                // 延時(shí) delayInMillis ms 后調(diào)用 complete#resume 方法通知該任務(wù)已經(jīng)執(zhí)行完成了.
                executorService.schedule(() -> {
                    complete.resume();
                    executorService.shutdown();
                }, delayInMillis, TimeUnit.MILLISECONDS);
            }
        };

        continuation.resume();
    }

    private static void doSomething(Continuation complete) {
        Continuation continuation = new Continuation() {

            int label = 0;

            @Override
            public void resume() {
                switch (label) {
                    case 0:
                        // 片段任務(wù) A
                        label = 1;
                        System.out.println("A");
                        customDelay(this, 10L);
                        return;

                    case 1:
                        // 片段任務(wù) B
                        label = 2;
                        System.out.println("B");
                        customDelay(this, 10L);
                        return;

                    case 2:
                        // 片段任務(wù) C
                        label = 3;
                        System.out.println("C");
                        break;
                }

                complete.resume();
            }
        };

        continuation.resume();
    }

    /**
     * Example 2.2 模擬 kotlin 協(xié)程標(biāo)準(zhǔn)庫(kù)啟動(dòng)一個(gè)協(xié)程.
     */
    public static void main(String[] args) {
        doSomething(new Continuation() {
            @Override
            public void resume() {
                System.out.println(">>> doSomething Completed <<<");
            }
        });
    }
}

/* output */
A
B
C
>>> doSomething Completed <<<

通過(guò)反編譯的代碼, 我們可以看出 Kotlin 編譯器做了以下幾個(gè)點(diǎn).

  1. 每一個(gè)掛起函數(shù)都被編譯成了一個(gè) Continuation.
  2. 每一個(gè)掛起函數(shù)都被編譯器添加了一個(gè) Continuation 參數(shù). 在完成該函數(shù)的任務(wù)之后, 會(huì)回調(diào)該參數(shù)的 #resume 方法. 該參數(shù)在 Kotlin 的源碼中是被隱藏的, 所以自定義掛起函數(shù)的時(shí)候需要使用 public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T 函數(shù)把隱藏的 Continuation 參數(shù)暴露出來(lái), 以便通知調(diào)用者任務(wù)已經(jīng)完成了.
  3. 如果掛起函數(shù)中有掛起點(diǎn), 被編譯成的 Continuation 中的 #resume 方法會(huì)被實(shí)現(xiàn)成狀態(tài)機(jī)模式. 兩兩掛起點(diǎn)之間組成一種狀態(tài). 在上面例子中, 我們可以清晰的看到 println("A") 到第一個(gè) #costomDelay 方法之間組成了第一種狀態(tài), println("B") 到第二個(gè) #customDelay 方法之間組成了第二種狀態(tài), 最后的 println("C") 組成最后一種狀態(tài).
  4. 在一個(gè)掛起函數(shù)調(diào)用另外一個(gè)掛起函數(shù)時(shí), 需要把自身作為參數(shù)傳入另外一個(gè)掛起函數(shù)中. 當(dāng)另外一個(gè)掛起函數(shù)完成時(shí), 會(huì)回調(diào)參數(shù)的 #resume 方法通知調(diào)用者繼續(xù)完成任務(wù). 例如在上面的例子中, 從 #doSomething函數(shù)調(diào)用 #customDelay 函數(shù)對(duì)應(yīng)代碼 customDelay(this, 10L); 中可以看出 #doSomething 把自身 this 作為參數(shù)傳給了 #customDelay 方法, 而最終自定義的 #customDelay 方法在等待任務(wù)結(jié)束后通過(guò) complete.resume(); 這句代碼讓 #doSomething 函數(shù)繼續(xù)運(yùn)行. 順帶一提, Kotlin 規(guī)定 suspend 修飾的函數(shù)或 lambda 表達(dá)式只能在 suspend 修飾的函數(shù)或表達(dá)式中調(diào)用. 就是因?yàn)?suspend 修飾的函數(shù)或 lambda 表達(dá)式會(huì)編譯成需要 Continuation 參數(shù)的 Continuation, 調(diào)用另外一個(gè) suspend 修飾的函數(shù)或 lambda 表達(dá)式需要傳入一個(gè) Continuation 作為參數(shù), 而只有在 suspend 修飾的函數(shù)或 lambda 表達(dá)式中才有 Continuation (自身) 對(duì)象傳入另外一個(gè) suspend 函數(shù)中.

至此, 我們已經(jīng)清晰的了解到了協(xié)程是怎么實(shí)現(xiàn)等待而不阻塞線程的了. 總結(jié)成一句話就是 suspend 關(guān)鍵字修飾的方法或 lambda 表達(dá)式會(huì)編譯成一個(gè)帶 Continuation 參數(shù)的 Continuation 對(duì)象, 當(dāng)一個(gè) Continuation 調(diào)用另外一個(gè) Continuation 時(shí)需要把自身作為參數(shù)傳入到另外一個(gè) Continuation 中, 另外一個(gè) Continuation 完成任務(wù)后會(huì)傳入的 Continuation 參數(shù)的 #resume 方法讓調(diào)用者繼續(xù)運(yùn)行. 更簡(jiǎn)單的說(shuō), 協(xié)程的掛起就是通過(guò) Continuation 這個(gè)回調(diào)對(duì)象實(shí)現(xiàn)的.


協(xié)程的使用指導(dǎo)

本片文章的初衷就是讓大家初步認(rèn)識(shí)一下協(xié)程長(zhǎng)什么樣子, 協(xié)程是什么東西, 協(xié)程的掛起原理是什么. 搞明白了這個(gè)三個(gè)問(wèn)題, 那么本片文章的目的也就達(dá)到了. 如果有興趣繼續(xù)學(xué)習(xí) Kotlin 協(xié)程相關(guān)內(nèi)容, 這里整理了一些 Kotlin 協(xié)程相關(guān)的官方文檔資料. 資料按易難程度順序排列.

最后的最后, 如果覺(jué)得本文對(duì)你有幫助, 請(qǐng)幫我點(diǎn)個(gè)??. 謝謝大家 ^ _ ^. 本文歡迎分享和轉(zhuǎn)載, 轉(zhuǎn)載請(qǐng)補(bǔ)上鏈接并注明出處.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容