理解 Kotlin 的協(xié)程

這篇文章會介紹 Kotlin 協(xié)程各個部分的作用,和常見類/函數(shù)/對象的用法。

從一個最簡單的例子看起:

fun main() {

    GlobalScope.launch {
        println("協(xié)程中的線程是:" + Thread.currentThread().name)
    }

    // 等待1秒鐘,讓協(xié)程執(zhí)行完
    Thread.sleep(1000)
}

這段代碼是在協(xié)程中打印所在線程的名稱,其中 GlobalScope 是協(xié)程默認(rèn)的全局作用域,launch 是一個協(xié)程構(gòu)造器,它創(chuàng)建了一個協(xié)程并自動執(zhí)行。下面我們詳細(xì)了解下協(xié)程的這三個部分:
(1) 協(xié)程作用域(CoroutineScope);
(2) 協(xié)程構(gòu)造器;
(3) 協(xié)程上下文(CoroutineContext) 和調(diào)度器(CoroutineDispatcher);


1. 協(xié)程作用域

例子中的 GlobalScope 雖然首字母大寫,但它是一個單例對象,是默認(rèn)的全局作用域。
GlobalScope 實現(xiàn)了 CoroutineScope 接口,這個接口持有了協(xié)程上下文,定義如下:

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

1.1 協(xié)程作用域的作用

作用域的主要作用是滿足結(jié)構(gòu)化并發(fā)的需求。
每個線程啟動后,它執(zhí)行的上下文就是整個進(jìn)程,沒有線程獨立的作用域和任務(wù)邊界。
但對于協(xié)程,我們很少需要一個全局的協(xié)程,協(xié)程總是與應(yīng)用程序中的某個局部作用域相關(guān),這個局部作用域是一個生命周期有限的實體,例如一次網(wǎng)絡(luò)加載、一個 Activity 或 Fragment。
想更好地理解什么是「結(jié)構(gòu)化并發(fā)」可以看這篇文章:《什么是結(jié)構(gòu)化并發(fā) 》。

1.2 如何自定義作用域?

協(xié)程作用域的創(chuàng)建方式有很多,常見的有:
① 繼承 CoroutineScope 接口自己實現(xiàn);
② 使用 coroutineScope 方法創(chuàng)建;
③ 使用 supervisorScope 方法創(chuàng)建;

下面分別看看:

(1) 繼承 CoroutineScope 接口實現(xiàn)自定義的作用域
如果你有一個業(yè)務(wù)相對獨立的類,你可以繼承 CoroutineScope 接口,使得你這個類成為一個協(xié)程的作用域。
例如 Android 開發(fā)中每一個 Activity 都可以是一個作用域:

class MainActivity: AppCompatActivity(), CoroutineScope {

    /** 需要實現(xiàn)協(xié)程上線文,這里使用空上下文 */
    override val coroutineContext = EmptyCoroutineContext

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 在默認(rèn)的子線程中,請求網(wǎng)絡(luò)數(shù)據(jù)
        launch {
            val res = requestService()

            // 在主線程中,更新 UI
            launch(Dispatchers.Main) {
                updateUi(res)
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        // 在 Activity 銷毀時取消協(xié)程
        cancel()
    }
}

為了在 Android/JavaFx 等場景中更方便的使用,官方提供了 MainScope() 方法快速創(chuàng)建基于主線程的協(xié)程作用域。
只需要將 CoroutineScope 的實現(xiàn)通過 by 關(guān)鍵字委托給 MainScope 對象即可:

(2) 使用 MainScope 創(chuàng)建協(xié)程作用域

class MainActivity: AppCompatActivity(), CoroutineScope by MainScope() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 在IO線程中,請求網(wǎng)絡(luò)數(shù)據(jù)
        launch(Dispatchers.IO) {
            val res = requestService()

            // 在主線程中,更新 UI
            launch {
                updateUi(res)
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()

        // 在 Activity 銷毀時取消
        cancel()
    }
}

(3) 使用 coroutineScope 和 supervisorScope 方法創(chuàng)建協(xié)程作用域
coroutineScope 方法可以用來創(chuàng)建一個子作用域,它只能在另一個已有的協(xié)程作用域中調(diào)用,例如在另外一個 suspend 方法中調(diào)用。
supervisorScope 方法和 coroutineScope 類似,也用于創(chuàng)建一個子作用域,
區(qū)別是 supervisorScope 出現(xiàn)異常時不影響其他子協(xié)程, coroutineScope 出現(xiàn)異常時會把異常拋出。
篇幅所限,不舉例子了。


2. 協(xié)程構(gòu)造器

例子中的 launch 就是一個攜程構(gòu)造器,利用協(xié)程構(gòu)造器可以方便地構(gòu)造出一個協(xié)程對象。
在創(chuàng)建線程時,我們可以這樣創(chuàng)建:

import kotlin.concurrent.thread
thread {
    // 在子線程中執(zhí)行的代碼
}

同樣,創(chuàng)建協(xié)程時,我們可以利用 launch、asyncrunBlocking、withContext 等構(gòu)造器創(chuàng)建協(xié)程,例如:

GlobalScope.launch {
    // 在協(xié)程中執(zhí)行的代碼
}

2.1 launch

launch 是最常見的協(xié)程構(gòu)建器,它會啟動一個新的協(xié)程(AbstractCoroutine),并將這個協(xié)程對象返回,接著會在協(xié)程中執(zhí)行參數(shù)中的 block。
AbstractCoroutine 繼承了 Job,launch 返回的 Job 對象實際就是協(xié)程對象本身。

launch 的原型如下:

public fun CoroutineScope.launch(
    /** 上下文 */
    context: CoroutineContext = EmptyCoroutineContext,
    
    /** 如何啟動 */
    start: CoroutineStart = CoroutineStart.DEFAULT,
    
    /** 啟動后要執(zhí)行的代碼 */
    block: suspend CoroutineScope.() -> Unit
): Job

launch 方法有兩個可選參數(shù):CoroutineContext 和 CoroutineStart。
CoroutineContext:
是協(xié)程的上下文,默認(rèn)使用 EmptyCoroutineContext,作用是決定把協(xié)程派發(fā)到哪個線程中執(zhí)行,下文會介紹。
CoroutineStart:
是啟動時刻的枚舉,默認(rèn)使用 CoroutineStart.DEFAULT,表示盡快執(zhí)行。

2.2 async

async 比較常見,它也會啟動新的協(xié)程(AbstractCoroutine),并返回這個協(xié)程對象,然后在協(xié)程中執(zhí)行 block。
返回類型 Deferred 繼承自 Job,與 Job 的區(qū)別是 Job 不會攜帶返回值, Deferred 帶了返回值。
所以 async 多用于需要返回結(jié)果的場景。

async 的函數(shù)原型:

public fun <T> CoroutineScope.async(
    /** 上下文 */
    context: CoroutineContext = EmptyCoroutineContext,
    
    /** 如何啟動 */
    start: CoroutineStart = CoroutineStart.DEFAULT,
    
    /** 啟動后要執(zhí)行的代碼 */
    block: suspend CoroutineScope.() -> Unit
): Deferred<T>

參數(shù)和 launch 一樣,我們看看 async 怎么獲取返回值:

// 任務(wù)1:耗時一秒后返回100
val coroutine1 = GlobalScope.async {
    delay(1000)
    return@async 100
}

// 任務(wù)2:耗時1秒后返回200
val coroutine2 = GlobalScope.async {
    delay(1000)
    return@async 200
}

// 上面兩個協(xié)程會并發(fā)執(zhí)行

// 等待兩個任務(wù)都執(zhí)行完畢后,再繼續(xù)下一步(打印結(jié)果)。
GlobalScope.launch {
    val v1 = coroutine1.await()
    val v2 = coroutine2.await()
    log("執(zhí)行的結(jié)果,v1 = $v1, v2=$v2")
}

async 還有更多用法,后面會介紹。

2.3 runBlocking

runBlocking 會啟動新的協(xié)程(AbstractCoroutine),并返回這個協(xié)程對象,然后在協(xié)程中執(zhí)行 block。
launchasync 不同的是,runBlocking 會阻塞住當(dāng)前線程,直到 block 執(zhí)行完畢。

runBlocking 不應(yīng)該在協(xié)程中調(diào)用,它大多數(shù)使用場景是為了測試。

runBlocking 的函數(shù)原型:

public fun <T> runBlocking(
    context: CoroutineContext = EmptyCoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

2.4 withContext

withContext 會在調(diào)用處掛起,直到 block 執(zhí)行完畢。它需要指定一個協(xié)程上下文,block 會在這個上下文中執(zhí)行。

withContext 的函數(shù)原型:

public suspend fun <T> withContext(
    context: CoroutineContext,
    block: suspend CoroutineScope.() -> T
): T

使用示例:

runBlocking {

    launch {
        delay(1000)
        log("第 1 處的代碼執(zhí)行完畢!")
    }

    withContext(GlobalScope.coroutineContext) {
        log("第 2 處的代碼開始執(zhí)行!")
        delay(2000)
        log("第 2 處的代碼執(zhí)行完畢!")
    }

    log("外部代碼執(zhí)行完畢!")
}

上面代碼的輸出結(jié)果是:

[Thread: 1 ] [12:32:23.772] 協(xié)程 2 中的代碼開始執(zhí)行!
[Thread: 1 ] [12:32:24.784] 協(xié)程 1 中的代碼執(zhí)行完畢!
[Thread: 1 ] [12:32:25.779] 協(xié)程 2 中的代碼執(zhí)行完畢!
[Thread: 1 ] [12:32:25.779] 外部代碼執(zhí)行完畢!

可以看到,withContext 調(diào)用后,后續(xù)的代碼會等到 withContext 的 block 執(zhí)行完畢后再執(zhí)行。 同時,線程是沒有被阻塞的。


3. 協(xié)程上下文 和 調(diào)度器

CoroutineContext 是一個接口,定義協(xié)程的上下文。
CoroutineDispatcher 負(fù)責(zé)決定將協(xié)程放到哪個線程中去執(zhí)行。

調(diào)度器(CoroutineDispatcher) 是上下文(CoroutineContext) 的子類。

Kotlin 提供了默認(rèn)的幾個調(diào)度器,放在 Dispatchers 中,他們分別是:

3.1 Dispatchers 中包含的調(diào)度器

(1) Dispatchers.Default
默認(rèn)的派發(fā)器。它會使用后臺共享的線程池。

(2) Dispatchers.Main
使用主線程。只有在 Android、JavaFx 等平臺才有。需要引入對應(yīng)的依賴:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5'

(3) Dispatchers.Unconfined
這個協(xié)程派發(fā)器會在調(diào)用者線程內(nèi)啟動協(xié)程, 但只會持續(xù)運行到第一次掛起點為止。
在掛起之后, 它會在哪個線程內(nèi)恢復(fù)執(zhí)行, 完全由被調(diào)用的掛起函數(shù)來決定。
非受限派發(fā)器(Unconfined dispatcher) 適用的場景是, 協(xié)程不占用 CPU 時間,
也不更新那些限定于某個特定線程的共享數(shù)據(jù)(比如 UI).

(4) Dispatchers.IO
用于執(zhí)行 IO 類型的協(xié)程。它會和 Dispatchers.Default 共享線程池。
所以在一個 Dispatchers.Default 的作用域中,試圖切換到 Dispatchers.IO 中執(zhí)行,線程不一定會切換。例如:

除了繼承了 上下文(CoroutineContext) 的調(diào)度器,還有幾個常見的 CoroutineContext 子類:

3.2 EmptyCoroutineContext

空上下文。
launch等協(xié)程構(gòu)造器使用的默認(rèn)上下文就是這個。
當(dāng)使用這個對象時,表示當(dāng)前的 block 執(zhí)行在父協(xié)程的上下文。

例如:

fun main() {
    runBlocking(context = ctx) {
    
        // 這里沒有指定上下文,默認(rèn)使用 EmptyCoroutineContext,
        // 也就是使用 runBlocking 的上下文 ctx
        GlobalScope.launch {}
    }
}

如果沒有父協(xié)程,會新創(chuàng)建一個線程池,例如:

fun main () {

    // 外層沒有父協(xié)程,且使用 EmptyCoroutineContext,
    // 則內(nèi)部會新建一個線程池
    GlobalScope.launch {}
}

3.3 newSingleThreadContext

當(dāng)使用這個參數(shù)時,表示當(dāng)前 block 執(zhí)行在新的協(xié)程上下文中,例如:

fun main () {
    
    val ctx = newSingleThreadContext("新的協(xié)程")
    GlobalScope.launch(context = ctx) {
        log("在新協(xié)程中執(zhí)行")
        // 不再使用時,需要手動 close 掉,節(jié)省線程資源
        ctx.close()
    }
}

注意:我們手動創(chuàng)建的協(xié)程上下文,一定要在不用時 close 掉。

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

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

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