如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 DevTech Ascendancy Hub
本篇文章對應的是 應用 Kotlin 協程:對比 Thread、創建協程、任務掛起 | Dispatcher、CoroutineContext、CoroutineScope
協程 Coroutine
是輕量級的執行序,它並不會綁定特定執行序(有可能掛起前在 A Thread, 恢復時轉為 B Thread 上下文)
Coroutine
與一般 Thread
比起來:
普通 Thread 是透過 CPU 發出中斷信號時做上下文切換,而 Coroutine 則是靠程式自身實現(通常會有 Library 可以用),不需要 CPU 控制(跟硬體沒關係)
普通 Thread 使用「堵塞」機制,Coroutine 使用「掛起」機制
機制 | 行為特色 | CPU 使用概念 |
---|---|---|
堵塞 | 讓出 CPU 時間,並在滿足條件後「被動喚醒」,如果條件不滿足就一直堵塞;Thread#wait() 概念 |
釋放 CPU |
掛起 | 讓出 CPU 時間,並在一定時間後「主動喚醒」去檢查條件是否滿足 | 仍佔用 CPU |
Kotlin Coroutine 重點抽象類
Coroutine 相關類 | 說明 | 補充 |
---|---|---|
CoroutineDispather |
協程調度器;決定 Coroutine 內的任務要在哪個 Thread 中運行! | - |
CoroutineContext |
協程上下文;所有 Corotine 都要在 CoroutineContext 範圍內 | 包含一個默認的 CoroutineDispather |
CoroutineScope |
協程作用域;CoroutineScope 會在具有生命週期上的實體實現(GlobalScope 則是 top-level 是整個程式的生命週期) | 它包含了一個 CoroutineContext |
Kotlin Coroutine 任務管理類
Coroutine 任務管理類 | 說明 | 補充 |
---|---|---|
Job |
任務執行的過程被封裝成 Job,之後這個 Job 會交由 CoroutineDispather 執行 | Job 具有簡單的生命週期,可被執行 & 取消 …,其中也有父子類關係,父 Job 可以管控子 Job |
Deferred |
Job 的拓展類;可以讓 Job 有返回值 | Deferred 拓展 Job |
Kotlin Coroutine 概念 & 關鍵字
Coroutine 概念 & 關鍵字 | 說明 | 補充 |
---|---|---|
suspend (關鍵字) |
表明該函數被執行時會被掛起!(掛起,但不是放 CPU) | suspend 關鍵字只能被使用在 CoroutineContext |
suspend point (概念) |
每個掛起的函數都稱為 suspend point | IDE 中標示圖案 |
Continuation (概念) |
兩個 suspend point 之間都稱為 Continuation | 兩個 suspend point 之間的程式是運行在外部的 CoroutineScope |
Coroutine 的創建有多種方式,以下我們就來介紹解個 Coroutine 創建的方式
launch
創建 Coroutine:它創建一個 CoroutineScope 當作上下文,將其賦予最後一個 Lambda 參數,再 返回 Job 對象(可透過 Job 來管理協程任務)
async
創建 Coroutine:它同樣會創建 CoroutineScope 賦予最後一個 Lambda 參數作為上下文,再 返回 Deferred 對象
而它與 launch
函數的不同點在於,launch
在呼叫時就會啟動協程,而 async
可以在需要的時候再啟動協程
runBlocking
函數:runBlocking 會堵塞當前 Thread 直到任務結束,較多使用在測試上Coroutine 預設在創建後會自動啟動任務;這其中可以透過 start
參數(launch
, async
都可以)來控制 Coroutine 的啟動方式
以下為
launch
源碼
我們可以透過設定 start 參數為 CoroutineStart.LAZY
可以達到 懶加載功能(它就不會自動啟動),之後只有在呼叫 start
、join
、await
後才會開始任務
Kotlin 中使用 suspend
關鍵字來標記一個掛起函數,在該函數執行完之前,不會執行下一行程式 (類似一種「同步」操作)
Kotlin 的掛起函數是採用 「CPS, Continuation Passing Style
」 與「狀態機」實現,保證會執行完掛起函數後,再往下執行
delay
函數會將當前 CoroutineScope 中的 狀態進行暫停,它類似於 Thread#sleep 方法,但 delay
它其實暫停的是協程不是執行序(仍尚未放開 CPU 的使用權!)
以下我們透過 launch
函數來啟動一個新的 CoroutineScope,來觀察 delay 到底會不會暫停 Thread(同時我們也可以觀察到 launch
會不創建一個新的執行序)
從下圖結果中,我們可以看到
launch
創建了一個新執行序,並且delay
函數並不會影響到外部執行序,而是影響到 Coroutine 創建的執行序(請觀察執行順序)
在 Java 中 Thread#yield
方法代表的意思是讓當前 Thread 讓出資源,並給其他 Thread 進行搶奪這個資源的使用權 (有可能其他 Thread 或自身搶到)
Thread#yied
通常使用的較少,因為它的可控性較低
Koltin 中 yield
函數也是差不多的意思,不過差異點在於:協程讓出資源後會將當前協程分發到 CoroutineDispatcher 的對列中做等待,等其他協程執行完才會執行自身協程(也就是讓出,並向後排隊的概念)
以下開啟兩個協程來互讓 (yield
) 資源,觀察它們讓出後排序的行為,是否會跟 Thread#yield
一樣隨機亂序
從下圖解果中,我們可以看出協程的 yield
更加的可控
CoroutineScope 的重要性、特性:CoroutineScope 會有關到協程的執行影響範圍;我們將上面的例子稍加修改,用不同的 CoroutineScope 去呼叫 yield
,我們可以觀察到 yield
就不會等待 (因為上下文已經不同)
launch
會繼承上層的 CoroutineScope,所以輸出的就是 Thread 就是 MainThread (因為內部的協程在同一個 CoroutineContext 中)協程中的 withContext
用於切換該協程要在哪個 ContextDispatcher 中運行,並且它也是一個掛起操作 (它也可以返回一個值,如同 runBlocking)
不同的 Dispatcher
差異
Dispatcher 種類 | 啟動協程的 Thread | 執行協程時使用的 Thread |
---|---|---|
Default |
被調用的 Thread | 協程的公用執行序池 |
IO |
被調用的 Thread | 協程的 IO 密集操作執行序池 |
Unconfined |
被調用的 Thread | 默認運行在當前協程的執行序,但如果碰到第一個暫停點(suspend point )後,它會運行在 任意執行序中 |
withContext
預設是可以取消的,但我們也可以配合 使用 NonCancelable
(它是一個 CoroutineContext) 來讓該協程不可被取消
可以看到任務在 100ms 後被取消,但是實際上還是有輸出
coroutineScope
同樣採用父協程的 CoroutineContext,並且它與 withContext
不同,它無法設定其他的 CoroutineDispatcher
調度器代表的是該協程任務會在哪個執行序中運行
以下創建多個不同的協程,並設定不同調度器,在觀察其運行時是使用哪個執行序執行
Coroutine 預設有提供我們以下幾種 Dispatcher
Dispatcher 種類 | 啟動協程的 Thread | 執行協程時使用的 Thread |
---|---|---|
Default |
被調用的 Thread | 協程的公用執行序池 |
IO |
被調用的 Thread | 協程的 IO 密集操作執行序池 |
Unconfined |
被調用的 Thread | 默認運行在當前協程的執行序,但如果碰到第一個暫停點(suspend point )後,它會運行在 任意執行序中 |
其中,Unconfined
是的特性比較特別,所以我們這邊來測試一下 Unconfined
的情況
從結果中我們可以看到,尚未碰到暫停點時,它就運行在呼叫者的執行序,當碰到暫停點後,就跑去其他執行序運作!
可以透過 CoroutineContext 來「區分協程之間的關係」,只有在相同 CoroutineContext 中的協程才會有父子關係… 範例如下
從下圖結果中我們可以看出
相同 CoroutineContext 之下的程式,會等待同一個 CoroutineContext 下的任務都執行完成 (parentContext
會等待 childContext
)
不同的 CoroutineContext 則不會等待(parentContext
不會等待 diffContext
)
+
符號來添加 CoroutineContext (有複寫這個 Operator);透過 MultiCoroutineContext 可以讓無關係的 CoroutineContext 產生關係
首先我們先看兩個完全不同的 CoroutineContext 執行的結果,看看兩者是否會有連結關係;
如果兩個 Coroutine 有連結關係的話,會等待內部 Coroutine 否則則不會等待
可以觀察到無關係的 diffContext
生命週期與 parentContext
同步 !
再來,我們透過 MultiCoroutineContext 的方式,將多個 Coroutine 產生生關聯
以下,透過創建一個 Job 來管理多個不同的 CoroutineContext (同樣透過 +
號添加)
只要管理的 Job 被關閉後,所有相關的任務都會被關閉
上面使用到的 launch
、async
都是 CoroutineScope
的拓展函數;而 GlobalScope
則是 CoroutineScope 的實現類
GlobalScope
是 top-level 函數,所以沒有綁定 Job 對象,其生命週期跟整個應用程式存亡首先我們先啟動一個 Coroutine,並在內部拋出異常,並且外部用 try/catch
包裹,來觀察是否可以抓取到異常
下圖中,我們可以看到在 Coroutine 中拋出異常,可以發現 外部的 try/catch
是捕捉不到!
這裡有個要注意的點,就是「協程的異常是會傳遞的」,子協程發生異常會向父協程拋出,子協程自身並捕捉不到
要捕捉協程中的異常可以使用 CoroutineExceptionHandler
,將它設定成 CoroutineContext,就可以捕捉協程異常
coroutineScope 它不能捕捉到子協程的異常
supervisorScope
比較特別,它會將異常「交由子協程自己處理」,所以這時候子協程才能捕捉到異常
如果協程使用 async / withContext,而在其中拋出錯誤,CoroutineExceptionHandler
是無法捕捉到的,必須自己用 Try/Catch 包裹
必須手動捕捉其錯誤! 雖然可以捕捉,但 發生錯誤之後該協程就不會再繼續後面的任務了
在這裡,我們提供了一系列豐富且深入的 Kotlin 語言相關文章,涵蓋了從基礎到進階的各個方面。讓我們一起來探索這些精彩內容!
Kotlin 語言基礎:想要建立堅實的 Kotlin 基礎?以下這些文章將帶你深入探索 Kotlin 的關鍵基礎和概念,幫你打造更堅固的 Kotlin 語言基礎
Kotlin 特性、特點:探索 Kotlin 的獨特特性和功能,加深對 Kotlin 語言的理解,並增強對於語言特性的應用
Kotlin 進階:協程、響應式、異步:若想深入學習 Kotlin 的進階主題,包括協程應用、Channel 使用、以及 Flow 的探索,請查看以下文章
Kotlin