Kotlin 協(xié)程的取消機(jī)制超詳細(xì)解讀

在 Java 語(yǔ)言中提供了線程中斷的能力,但并不是所有的線程都可以中斷的,因?yàn)?interrupt 方法并不是真正的終止線程,而是將一個(gè)標(biāo)志位標(biāo)記為中斷狀態(tài),當(dāng)運(yùn)行到下一次中斷標(biāo)志位檢查時(shí),才能觸發(fā)終止線程。

但無(wú)論如何,終止線程是一個(gè)糟糕的方案,因?yàn)樵诰€程的銷(xiāo)毀和重建,是要消耗系統(tǒng)資源的,造成了不必要的開(kāi)銷(xiāo)。Kotlin 協(xié)程提供了更優(yōu)雅的取消機(jī)制,這也是協(xié)程比較核心的功能之一。

協(xié)程的狀態(tài)

在了解取消機(jī)制之前我們需要知道一些關(guān)于 Job 狀態(tài)的內(nèi)容:

State isActive(是否活躍) isCompleted(是否完成) isCancelled(是否取消)
New (可選初始狀態(tài)) false false false
Active (默認(rèn)初始狀態(tài)) true false false
Completing (短暫態(tài)) true false false
Cancelling (短暫態(tài)) false false true
Cancelled (完成態(tài)) false true true
Completed (完成態(tài)) false true false

可以看出,在完成和取消的過(guò)程中,會(huì)經(jīng)過(guò)一個(gè)短暫的進(jìn)行中的狀態(tài),然后才變成已完成/已取消。

在這里只關(guān)注一下取消相關(guān)的狀態(tài):

  • Cancelling

    拋出異常的 Job 會(huì)導(dǎo)致其進(jìn)入 Cancelling 狀態(tài),也可以使用 cancel 方法來(lái)隨時(shí)取消 Job 使其立即轉(zhuǎn)換為 Cancelling 狀態(tài)。

  • Cancelled

    當(dāng)它遞歸取消子項(xiàng),并等待所有的子項(xiàng)都取消后,該 Job 會(huì)進(jìn)入 Cancelled 狀態(tài)。

取消協(xié)程的用法

協(xié)程在代碼中抽象的類(lèi)型是 Job , 下面是一個(gè)官方的代碼示例,用來(lái)展示如何取消協(xié)程的執(zhí)行:

suspend fun main(): Unit = coroutineScope {
    val job = launch {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancel() // cancels the job
    job.join() // waits for job's completion 
    println("main: Now I can quit.")
}

它的輸出是:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.

一旦 mian 方法中調(diào)用了 job.cancel() ,我們就看不到其他協(xié)程的任何輸出,因?yàn)樗驯蝗∠恕?/p>

協(xié)程取消的有效性

協(xié)程代碼必須通過(guò)與掛起函數(shù)的配合才能被取消。kotlinx.coroutines 中所有掛起函數(shù)(帶有 suspend 關(guān)鍵字函數(shù))都是可以被取消的。suspend 函數(shù)會(huì)檢查協(xié)程是否需要取消并在取消時(shí)拋出 CancellationException 。

但是,如果協(xié)程在運(yùn)行過(guò)程中沒(méi)有掛起點(diǎn),則不能取消協(xié)程,如下例所示:

suspend fun main(): Unit = coroutineScope {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (i < 5) { // computation loop, just wastes CPU
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")
}

在這個(gè) job 中,并沒(méi)有執(zhí)行任何 suspend 函數(shù),所以在執(zhí)行過(guò)程中并沒(méi)有對(duì)協(xié)程是否需要取消進(jìn)行檢查,自然也就無(wú)法觸發(fā)取消。

同樣的問(wèn)題也可以在通過(guò) 捕獲 CancellationException 并且不拋出的情況下 觀察到:

suspend fun main(): Unit = coroutineScope {
    val job = launch(Dispatchers.Default) {
        repeat(5) { i ->
            try {
                // print a message twice a second
                println("job: I'm sleeping $i ...")
                delay(500)
            } catch (e: Exception) {
                // log the exception
                println(e)
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")
}

打印結(jié)果是:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@614acfe9
job: I'm sleeping 3 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@614acfe9
job: I'm sleeping 4 ...
kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@614acfe9
main: Now I can quit.

從打印結(jié)果來(lái)看,循環(huán) 5 次全部執(zhí)行了,好像取消并沒(méi)有起到作用。但實(shí)際上不是這樣的,為了便于觀察加上時(shí)間戳:

1665217217682: job: I'm sleeping 0 ...
1665217218196: job: I'm sleeping 1 ...
1665217218697: job: I'm sleeping 2 ...
1665217218996: main: I'm tired of waiting!
1665217219000: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@3a1efc0d
1665217219000: job: I'm sleeping 3 ...
1665217219000: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@3a1efc0d
1665217219000: job: I'm sleeping 4 ...
1665217219000: kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@3a1efc0d
1665217219001: main: Now I can quit.

加上時(shí)間可以看出,拋出第一次異常后的兩次循環(huán)和異常捕獲都是在同一瞬間完成的。這說(shuō)明了捕獲到異常后,仍然會(huì)執(zhí)行代碼,但是所有的 delay 方法都沒(méi)有生效,即該 Job 的所有子 Job 都失效了。但該 Job 仍在繼續(xù)循環(huán)打印。原因是,父 Job 會(huì)等所有子 Job 處理結(jié)束后才能完成取消。

而如果我們不使用 try-catch 呢?

suspend fun main(): Unit = coroutineScope {
    val job = launch(Dispatchers.Default) {
        repeat(5) { i ->
            // print a message twice a second
            println("job: I'm sleeping $i ...")
            delay(500)
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")
}

打印結(jié)果:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.

很順利的取消了,這是因?yàn)閰f(xié)程拋出 Exception 直接終止了。

注意協(xié)程拋出 CancellationException 并不會(huì)導(dǎo)致 App Crash 。

使用 try-catch 來(lái)捕獲 CancellationException 時(shí)需要注意,在掛起函數(shù)前的代碼邏輯仍會(huì)多次執(zhí)行,從而導(dǎo)致這部分代碼仿佛沒(méi)有被取消一樣。

如何寫(xiě)出可以取消的代碼

有兩種方法可以使代碼是可取消的。第一種方法是定期調(diào)用掛起函數(shù),檢查是否取消,就是上面的例子中的方法;另一個(gè)是顯式檢查取消狀態(tài):

suspend fun main(): Unit = coroutineScope {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // cancellable computation loop
            // print a message twice a second
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1300L) // delay a bit
    println("main: I'm tired of waiting!")
    job.cancelAndJoin() // cancels the job and waits for its completion
    println("main: Now I can quit.")
}

將上面的循環(huán) 5 次通過(guò)使用 while (isActive) 進(jìn)行替換,實(shí)現(xiàn)顯示檢查取消的代碼。isActive 是通過(guò) CoroutineScope 對(duì)象在協(xié)程內(nèi)部可用的擴(kuò)展屬性。

在 finally 中釋放資源

在前面的例子中我們使用 try-catch 捕獲 CancellationException 發(fā)現(xiàn)會(huì)產(chǎn)生父協(xié)程等待所有子協(xié)程完成后才能完成,所以建議不用 try-catch 而是 try{…} finally{…} ,讓父協(xié)程在被取消時(shí)正常執(zhí)行終結(jié)操作:

val job = launch {
    try {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    } finally {
        println("job: I'm running finally")
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")

join 和 cancelAndJoin 都要等待所有終結(jié)操作完成,所以上面的例子產(chǎn)生了以下輸出:

job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
job: I'm running finally
main: Now I can quit.

使用不可取消的 block

如果在在上面的示例的 finally 代碼塊中使用 suspend 函數(shù),會(huì)導(dǎo)致拋出 CancellationException 。

因?yàn)檫\(yùn)行這些代碼的協(xié)程已經(jīng)被取消了。通常情況下這不會(huì)有任何問(wèn)題,然而,在極少數(shù)情況下,如果你需要在 finally 中使用一個(gè)掛起函數(shù),你可以通過(guò)使用 withContext(NonCancellable) { ... }

val job = launch {
    try {
        repeat(1000) { i ->
            println("job: I'm sleeping $i ...")
            delay(500L)
        }
    } finally {
        withContext(NonCancellable) {
            println("job: I'm running finally")
            delay(1000L)
            println("job: And I've just delayed for 1 sec because I'm non-cancellable")
        }
    }
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")

CancellationException

在上面的內(nèi)容中,我們知道協(xié)程的取消是通過(guò)拋出 CancellationException 來(lái)進(jìn)行的,神奇的是拋出 Exception 并沒(méi)有導(dǎo)致應(yīng)用程序 Crash 。

CancellationException 的真實(shí)實(shí)現(xiàn)是 j.u.c. 中的 CancellationException :

public actual typealias CancellationException = java.util.concurrent.CancellationException

如果協(xié)程的 Job 被取消,則由可取消的掛起函數(shù)拋出 CancellationException 。它表示協(xié)程的正常取消。在默認(rèn)的 CoroutineExceptionHandler 下,它不會(huì)打印到控制臺(tái)/日志。

上面引用了這個(gè)類(lèi)的注釋,看來(lái)處理拋出異常的邏輯在 CoroutineExceptionHandler 中:

public interface CoroutineExceptionHandler : CoroutineContext.Element {
    /**
     * Key for [CoroutineExceptionHandler] instance in the coroutine context.
     */
    public companion object Key : CoroutineContext.Key<CoroutineExceptionHandler>

    /**
     * Handles uncaught [exception] in the given [context]. It is invoked
     * if coroutine has an uncaught exception.
     */
    public fun handleException(context: CoroutineContext, exception: Throwable)
}

通常,未捕獲的 Exception 只能由使用協(xié)程構(gòu)建器的根協(xié)程產(chǎn)生。所有子協(xié)程都將異常的處理委托給他們的父協(xié)程,父協(xié)程也委托給它自身的父協(xié)程,直到委托給根協(xié)程處理。所以在子協(xié)程中的 CoroutineExceptionHandler 永遠(yuǎn)不會(huì)被使用。

使用 SupervisorJob 運(yùn)行的協(xié)程不會(huì)將異常傳遞給它們的父協(xié)程,SupervisorJob 被視為根協(xié)程。

使用 async 創(chuàng)建的協(xié)程總是捕獲它的所有異常通過(guò)結(jié)果 Deferred 對(duì)象回調(diào)出去,因此它不能導(dǎo)致未捕獲的異常。

CoroutineExceptionHandler 用于記錄異常、顯示某種類(lèi)型的錯(cuò)誤消息、終止和/或重新啟動(dòng)應(yīng)用程序。

如果需要在代碼的特定部分處理異常,建議在協(xié)程中的相應(yīng)代碼周?chē)褂?try-catch。通過(guò)這種方式,您可以阻止異常協(xié)程的完成(異?,F(xiàn)在被捕獲),重試操作,和/或采取其他任意操作。 這也就是我們前面論證的在協(xié)程中使用 try-catch 導(dǎo)致的取消失效。

默認(rèn)情況下,如果協(xié)程沒(méi)有配置用于處理異常的 Handler ,未捕獲的異常將按以下方式處理:

  • 如果 exception 是 CancellationException ,那么它將被忽略(因?yàn)檫@是取消正在運(yùn)行的協(xié)程的假定機(jī)制)。

  • 其他情況:

    • 如果上下文中有一個(gè) Job,那么調(diào)用 job.cancel() 。
    • 否則,通過(guò) ServiceLoader 找到的 CoroutineExceptionHandler 的所有實(shí)例并調(diào)用當(dāng)前線程的 Thread.uncaughtExceptionHandler 來(lái)處理異常。

超時(shí)取消

取消協(xié)程執(zhí)行的最合適的應(yīng)用場(chǎng)景是它的執(zhí)行時(shí)間超過(guò)了規(guī)定的最大時(shí)間時(shí)自動(dòng)取消任務(wù)。在 Kotlin 協(xié)程庫(kù)中提供了 withTimeout 方法來(lái)實(shí)現(xiàn)這個(gè)功能:

withTimeout(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
}

執(zhí)行結(jié)果:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms

TimeoutCancellationException 是 CancellationException 的子類(lèi),TimeoutCancellationException 通過(guò) withTimeout 函數(shù)拋出。

在本例中,我們?cè)趍ain函數(shù)中使用了withTimeout ,運(yùn)行過(guò)程中會(huì)導(dǎo)致 Crash 。

有兩種解決辦法,就是使用 try{…} catch (e: TimeoutCancellationException){…} 代碼塊;另一種辦法是使用在超時(shí)的情況下不是拋出異常而是返回 null 的 withTimeoutOrNull 函數(shù):

val result = withTimeoutOrNull(1300L) {
    repeat(1000) { i ->
        println("I'm sleeping $i ...")
        delay(500L)
    }
    "Done" // will get cancelled before it produces this result
}
println("Result is $result")

打印結(jié)果:

I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null

異步的超時(shí)和資源

withTimeout 中的超時(shí)事件相對(duì)于在其塊中運(yùn)行的代碼是異步的,并且可能在任何時(shí)間發(fā)生,甚至在從超時(shí)塊內(nèi)部返回之前。如果你在塊內(nèi)部打開(kāi)或獲取一些資源,需要關(guān)閉或釋放到塊外部。

例如,在這里,我們用 Resource 類(lèi)模擬一個(gè)可關(guān)閉資源,它只是通過(guò)對(duì)獲得的計(jì)數(shù)器遞增,并對(duì)該計(jì)數(shù)器從其關(guān)閉函數(shù)遞減來(lái)跟蹤創(chuàng)建次數(shù)。讓我們用小超時(shí)運(yùn)行大量的協(xié)程,嘗試在一段延遲后從withTimeout塊內(nèi)部獲取這個(gè)資源,并從外部釋放它。

var acquired = 0

class Resource {
    init { acquired++ } // Acquire the resource
    fun close() { acquired-- } // Release the resource
}

fun main() {
    runBlocking {
        repeat(100_000) { // Launch 100K coroutines
            launch { 
                val resource = withTimeout(60) { // Timeout of 60 ms
                    delay(50) // Delay for 50 ms
                    Resource() // Acquire a resource and return it from withTimeout block     
                }
                resource.close() // Release the resource
            }
        }
    }
    // Outside of runBlocking all coroutines have completed
    println(acquired) // Print the number of resources still acquired
}

如果運(yùn)行上面的代碼,您將看到它并不總是打印 0,盡管它可能取決于您的機(jī)器的時(shí)間,在本例中您可能需要調(diào)整超時(shí)以實(shí)際看到非零值。

要解決這個(gè)問(wèn)題,可以在變量中存儲(chǔ)對(duì)資源的引用,而不是從withTimeout塊返回它。

fun main() {
    runBlocking {
        repeat(100_000) { // Launch 100K coroutines
            launch {
                var resource: Resource? = null // Not acquired yet
                try {
                    withTimeout(60) { // Timeout of 60 ms
                        delay(50) // Delay for 50 ms
                        resource = Resource() // Store a resource to the variable if acquired
                    }
                    // We can do something else with the resource here
                } finally {
                    resource?.close() // Release the resource if it was acquired
                }
            }
        }
    }
// Outside of runBlocking all coroutines have completed
    println(acquired) // Print the number of resources still acquired
}

這樣這個(gè)例子總是輸出0。資源不會(huì)泄漏。

取消檢查的底層原理

在探索協(xié)程取消的有效性時(shí),我們知道協(xié)程代碼必須通過(guò)與掛起函數(shù)的配合才能被取消。

kotlinx.coroutines 中所有掛起函數(shù)(帶有 suspend 關(guān)鍵字函數(shù))都是可以被取消的。suspend 函數(shù)會(huì)檢查協(xié)程是否需要取消并在取消時(shí)拋出 CancellationException 。

關(guān)于協(xié)程的取消機(jī)制,很明顯和 suspend 關(guān)鍵字有關(guān)。為了測(cè)試 suspend 關(guān)鍵字的作用,實(shí)現(xiàn)下面的代碼:

class Solution {
    suspend fun func(): String {
        return "測(cè)試 suspend 關(guān)鍵字"
    }
}

作為對(duì)照組,另一個(gè)是不加 suspend 關(guān)鍵字的 func 方法:

class Solution {
    fun func(): String {
        return "測(cè)試 suspend 關(guān)鍵字"
    }
}

兩者反編譯成 Java :

// 普通的方法
public final class Solution {
    public static final int $stable = LiveLiterals$SolutionKt.INSTANCE.Int$class-Solution();

    @NotNull
    public final String func() {
        return LiveLiterals$SolutionKt.INSTANCE.String$fun-func$class-Solution();
    }
}

// 帶有 suspend 關(guān)鍵字的方法
public final class Solution {
    public static final int $stable = LiveLiterals$SolutionKt.INSTANCE.Int$class-Solution();

    @Nullable
    public final Object func(@NotNull Continuation<? super String> $completion) {
        return LiveLiterals$SolutionKt.INSTANCE.String$fun-func$class-Solution();
    }
}

suspend 關(guān)鍵字修飾的方法反編譯后默認(rèn)生成了帶有 Continuation 參數(shù)的方法。說(shuō)明 suspend 關(guān)鍵字的玄機(jī)在 Continuation 類(lèi)中。

Continuation 是 Kotlin 協(xié)程的核心思想 Continuation-Passing Style 的實(shí)現(xiàn)。原理參考簡(jiǎn)述協(xié)程的底層實(shí)現(xiàn)原理

通過(guò)在普通函數(shù)的參數(shù)中增加一個(gè) Continuation 參數(shù),這個(gè) continuation 的性質(zhì)類(lèi)似于一個(gè) lambda 對(duì)象,將方法的返回值類(lèi)型傳遞到這個(gè) lambda 代碼塊中。

什么意思呢?就是本來(lái)這個(gè)方法的返回類(lèi)型直接 return 出來(lái)的:

val a: String = func()
print(a)

而經(jīng)過(guò) suspend 修飾,代碼變成了這個(gè)樣子:

func { a ->
    print(a)
}

Kotlin 協(xié)程就是通過(guò)這樣的包裝,將比如 launch 方法,實(shí)際上是 launch 最后一個(gè)參數(shù)接收的是 lambda 參數(shù)。也就是把外部邏輯傳遞給函數(shù)內(nèi)部執(zhí)行。

回過(guò)頭來(lái)再來(lái)理解 suspend 關(guān)鍵字,我們知道帶有 suspend 關(guān)鍵字的方法會(huì)對(duì)協(xié)程的取消進(jìn)行檢查,從而取消協(xié)程的執(zhí)行。從這個(gè)能力上來(lái)看,我理解他應(yīng)該會(huì)自動(dòng)生成類(lèi)似下面的邏輯代碼:

生成的函數(shù) {
    if(!當(dāng)前協(xié)程.isActive) {
        throw CancellationException()
    }
    // ... 這里是函數(shù)真實(shí)邏輯
}

suspend 修飾的函數(shù),會(huì)自動(dòng)生成一個(gè)掛起點(diǎn),來(lái)檢查協(xié)程是否應(yīng)該被掛起。

顯然 Continuation 中聲明的函數(shù)也證實(shí)了掛起的功能:

public interface Continuation<in T> {
    /**
     * The context of the coroutine that corresponds to this continuation.
     */
    public val context: CoroutineContext

    /**
     * 恢復(fù)相應(yīng)協(xié)程的執(zhí)行,將成功或失敗的結(jié)果作為最后一個(gè)掛起點(diǎn)的返回值傳遞。
     */
    public fun resumeWith(result: Result<T>)
}

協(xié)程本質(zhì)上是產(chǎn)生了一個(gè) switch 語(yǔ)句,每個(gè)掛起點(diǎn)之間的邏輯都是一個(gè) case 分支的邏輯。參考 協(xié)程是如何實(shí)現(xiàn)的 中的例子:

        Function1 lambda = (Function1)(new Function1((Continuation)null) {
            int label;

            @Nullable
            public final Object invokeSuspend(@NotNull Object $result) {
                byte text;
                @BlockTag1: {
                    Object result;
                    @BlockTag2: {
                        result = IntrinsicsKt.getCOROUTINE_SUSPENDED();
                        switch(this.label) {
                            case 0:
                                ResultKt.throwOnFailure($result);
                                this.label = 1;
                                if (SuspendTestKt.dummy(this) == result) {
                                    return result;
                                }
                                break;
                            case 1:
                                ResultKt.throwOnFailure($result);
                                break;
                            case 2:
                                ResultKt.throwOnFailure($result);
                                break @BlockTag2;
                            case 3:
                                ResultKt.throwOnFailure($result);
                                break @BlockTag1;
                            default:
                                throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
                        }

                        text = 1;
                        System.out.println(text);
                        this.label = 2;
                        if (SuspendTestKt.dummy(this) == result) {
                            return result;
                        }
                    }

                    text = 2;
                    System.out.println(text);
                    this.label = 3;
                    if (SuspendTestKt.dummy(this) == result) {
                        return result;
                    }
                }
                text = 3;
                System.out.println(text);
                return Unit.INSTANCE;
            }

            @NotNull
            public final Continuation create(@NotNull Continuation completion) {
                Intrinsics.checkNotNullParameter(completion, "completion");
                Function1 funcation = new <anonymous constructor>(completion);
                return funcation;
            }

            public final Object invoke(Object object) {
                return ((<undefinedtype>)this.create((Continuation)object)).invokeSuspend(Unit.INSTANCE);
            }
        });

可以看出,在每個(gè)分支都會(huì)執(zhí)行一次 ResultKt.throwOnFailure($result); ,從名字上就知道,這就是檢查是否需要取消并拋出異常的代碼所在:

@PublishedApi
@SinceKotlin("1.3")
internal fun Result<*>.throwOnFailure() {
    if (value is Result.Failure) throw value.exception
}

這里的 Result 類(lèi)是一個(gè)包裝類(lèi),它將成功的結(jié)果封裝為類(lèi)型 T 的值,或?qū)⑹〉慕Y(jié)果封裝為帶有任意Throwable異常的值。

        @Suppress("INAPPLICABLE_JVM_NAME")
        @InlineOnly
        @JvmName("success")
        public inline fun <T> success(value: T): Result<T> =
            Result(value)

        /**
         * Returns an instance that encapsulates the given [Throwable] [exception] as failure.
         */
        @Suppress("INAPPLICABLE_JVM_NAME")
        @InlineOnly
        @JvmName("failure")
        public inline fun <T> failure(exception: Throwable): Result<T> =
            Result(createFailure(exception))

成功和失敗的方法類(lèi)型是不一樣的,證實(shí)了這一點(diǎn),success 方法接收類(lèi)型為 T 的參數(shù);failure 接收 Throwable 類(lèi)型的參數(shù)。

到這里 suspend 方法掛起的原理就明了了:在協(xié)程的狀態(tài)機(jī)中,通過(guò)掛起點(diǎn)會(huì)分割出不同的狀態(tài),對(duì)每一個(gè)狀態(tài),會(huì)先進(jìn)行掛起結(jié)果的檢查。 這會(huì)導(dǎo)致以下結(jié)果:

  • 協(xié)程的取消機(jī)制是通過(guò)掛起函數(shù)的掛起點(diǎn)檢查來(lái)進(jìn)行取消檢查的。證實(shí)了為什么如果沒(méi)有 suspend 函數(shù)(本質(zhì)是掛起點(diǎn)),協(xié)程的取消就不會(huì)生效。
  • 協(xié)程的取消機(jī)制是需要函數(shù)合作的,就是通過(guò) suspend 函數(shù)來(lái)增加取消檢查的時(shí)機(jī)。
  • 父協(xié)程會(huì)執(zhí)行完所有的子協(xié)程(掛起函數(shù)),因?yàn)榇a的本質(zhì)是一個(gè)循環(huán)執(zhí)行 switch 語(yǔ)句,當(dāng)一個(gè)子協(xié)程(或掛起函數(shù))執(zhí)行結(jié)束,會(huì)繼續(xù)執(zhí)行到下一個(gè)分支。但是最后一個(gè)掛起點(diǎn)后續(xù)的代碼并不會(huì)被執(zhí)行,因?yàn)樽詈笠粋€(gè)掛起點(diǎn)檢查到失敗,不會(huì)繼續(xù)跳到最后的 label 分支。

作者:自動(dòng)化BUG制造器
鏈接:https://juejin.cn/post/7158008928930906148

?著作權(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)容