Kotlin Coroutines (協程)
並行程式設計 (Concurrency) 一直是痛點。從早期的 Thread,到 Callback Hell,再到 RxJava。 Kotlin 推出了 Coroutines,核心理念是:用寫同步程式碼的方式,來寫非同步程式。
這篇文章將帶你深入了解 Coroutines 的運作機制,而不只是皮毛。
核心觀念:什麼是協程?
你可以把它想像成 輕量級的 Thread (Lightweight Thread)。 一個 Thread 可以跑成千上萬個 Coroutine。
- Thread (執行緒):作業系統層級,建立與切換成本高。
- Coroutine (協程):語言層級 (Kotlin Runtime),成本極低,可以在執行緒之間「暫停 (Suspend)」和「恢復 (Resume)」。
非阻塞 (Non-blocking) 是關鍵: 當協程執行到網路請求時,它可以 暫停 自己,讓底下的 Thread 去做別的事(例如處理 UI)。等到網路資料回來了,協程再 恢復 執行。
關鍵字:suspend
suspend 修飾的函式稱為 掛起函式。
它具有神奇的能力:它可以暫停執行,但不會阻塞執行緒。
// 模擬一個耗時操作
suspend fun fetchUserData(): String {
delay(1000) // delay 是 suspend function,它不會卡住 Thread
return "User Miko"
}
規則: suspend 函式只能被「另一個 suspend 函式」或「協程作用域 (CoroutineScope)」呼叫。
啟動協程:Builders
要進入協程的世界,我們需要 Coroutine Builders。
launch (射後不理)
不會回傳結果,回傳一個 Job 物件用來控制(取消)協程。適合不需要回傳值的操作 (如更新 Cache)。
scope.launch {
// 執行任務
println("Task started")
}
async (取得結果)
會回傳結果 (Deferred<T>)。你需要呼叫 .await() 來取得最終的值。
適合並行處理多個任務。
scope.launch {
val deferred1 = async { fetchPart1() }
val deferred2 = async { fetchPart2() }
// 這裡會等待兩個都做完才繼續 (並行執行)
val result = deferred1.await() + deferred2.await()
}
runBlocking (阻塞當前執行緒)
不管是測試還是 main function 才會用到。 它會真的 卡住 (Block) 當前的 Thread 直到裡面的協程跑完。千萬不要在 UI Thread (Android) 或 Backend 的 Request Thread 中使用它。
fun main() = runBlocking {
launch {
delay(1000)
println("World!")
}
println("Hello,")
}
協程作用域 (CoroutineScope)
協程必須在 Scope 中執行。Scope 定義了協程的 生命週期。
常見的 Scope
- GlobalScope: 全域 Scope,生命週期跟隨整個 App。它不受控,強烈建議不要在專案中使用,因為容易造成 Memory Leak。
- CoroutineScope(Dispatchers.XX): 自定義 Scope。通常我們會繼承它或使用委派。
- MainScope(): 預設在 UI Thread 的 Scope。
Android 專用 Scope
- ViewModelScope: 跟隨
ViewModel生命週期。ViewModel 清除時自動取消所有協程。 - LifecycleScope: 跟隨
Activity/Fragment生命週期。
// Android 範例
viewModelScope.launch {
// 當使用者離開畫面,ViewModel 被 cleared,這裡的任務會自動被取消
// 不會因為網路請求回來但畫面不在了而 Crash
val data = repository.fetch()
}
調度器 (Dispatchers):決定在哪裡跑
你可以指定協程要在哪個執行緒池執行 (Context Switching)。
| Dispatcher | 用途 | 底層機制 |
|---|---|---|
| Dispatchers.Main | UI 操作 | Android 的 Main Thread (如 Swing/JavaFx 的 Event Loop) |
| Dispatchers.IO | I/O 密集型 | 共用的 Thread Pool (最多 64 個),適合讀寫檔案、網路請求、DB |
| Dispatchers.Default | CPU 密集型 | 共用的 Thread Pool (核心數),適合大量數學運算、影像處理 |
| Dispatchers.Unconfined | 不限制 | 呼叫在哪就在哪,直到第一次掛起。通常不建議使用。 |
最佳實踐:在合適的地方切換 Context
使用 withContext 來切換執行緒。
fun loadData() {
viewModelScope.launch(Dispatchers.Main) { // 1. 在 UI Thread 啟動
showLoading()
val result = withContext(Dispatchers.IO) { // 2. 切換到 IO Thread 跑耗時任務
api.getData() // 這行會卡住 IO Thread,但 Main Thread 是順暢的
}
// 3. 執行完自動切回 Main Thread
showResult(result)
}
}
結構化並發 (Structured Concurrency)
這是 Kotlin Coroutines 最重要的設計哲學。「協程必須有父子關係」。
- 父協程取消,子協程會全部被取消。
- 父協程會等待所有子協程完成,才會視為完成。
- 子協程拋出例外,會向上傳遞導致父協程取消 (除非使用
SupervisorJob)。
這避免了「孤兒協程 (Dangling Coroutines)」在背景偷跑或是吞掉錯誤的問題。
Job 與取消 (Cancellation)
所有 launch 都會回傳一個 Job。
val job = scope.launch {
while (isActive) { // 檢查是否還活著
// 做事...
}
}
// 取消任務
job.cancel()
isActive 或沒有呼叫 suspend 函式 (如 yield() 或 delay()),它是 無法被取消的。異步串流:Flow
如果你需要回傳「多個值」怎麼辦?(例如下載進度條、即時股票報價)。
List<T> 是一次給全部,Sequence<T> 是同步的。
非同步的多個值,就是 Flow (冷串流)。
建立 Flow
fun countdown(): Flow<Int> = flow {
for (i in 5 downTo 1) {
emit(i) // 發送值
delay(1000)
}
}
收集 Flow (Collect)
Flow 是 冷 (Cold) 的,你不 collect 它,裡面的 code 根本不會跑。
launch {
countdown()
.map { "剩餘 $it 秒" } // 支援各種操作元
.collect { value ->
println(value)
}
}
Flow 的強大之處在於它也支援 Backpressure (背壓) 處理與豐富的運算子 (debounce, combine, zip 等),完全可以取代 RxJava。