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.MainUI 操作Android 的 Main Thread (如 Swing/JavaFx 的 Event Loop)
Dispatchers.IOI/O 密集型共用的 Thread Pool (最多 64 個),適合讀寫檔案、網路請求、DB
Dispatchers.DefaultCPU 密集型共用的 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 最重要的設計哲學。「協程必須有父子關係」

  1. 父協程取消,子協程會全部被取消
  2. 父協程會等待所有子協程完成,才會視為完成。
  3. 子協程拋出例外,會向上傳遞導致父協程取消 (除非使用 SupervisorJob)。

這避免了「孤兒協程 (Dangling Coroutines)」在背景偷跑或是吞掉錯誤的問題。

Job 與取消 (Cancellation)

所有 launch 都會回傳一個 Job

val job = scope.launch {
    while (isActive) { // 檢查是否還活著
        // 做事...
    }
}

// 取消任務
job.cancel()
注意:協程的取消是「協作式 (Cooperative)」的! 如果你的協程正在跑一個死迴圈且沒有檢查 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。