跳至正文

🎛️ Context & Dispatcher

📖 什麼是 CoroutineContext?

CoroutineContext 是決定協程如何執行的設定集合。它指定在哪個執行緒上執行、名稱是什麼等等!

💡 Dispatcher

基本 Dispatcher

fun main() = runBlocking {
// Main - UI 執行緒 (Android/Desktop)
launch(Dispatchers.Main) {
// UI 更新
}

// IO - 網路/檔案操作
launch(Dispatchers.IO) {
println("IO: ${Thread.currentThread().name}")
}

// Default - CPU 密集型操作
launch(Dispatchers.Default) {
println("Default: ${Thread.currentThread().name}")
}

// Unconfined - 特殊情況
launch(Dispatchers.Unconfined) {
println("Unconfined: ${Thread.currentThread().name}")
}

delay(100)
}

Dispatchers.IO

suspend fun readFile(): String = withContext(Dispatchers.IO) {
// 讀取檔案、網路請求等
delay(1000)
"檔案內容"
}

suspend fun writeFile(content: String) = withContext(Dispatchers.IO) {
// 寫入檔案
delay(500)
println("儲存檔案: $content")
}

fun main() = runBlocking {
val content = readFile()
writeFile(content)
}

Dispatchers.Default

suspend fun heavyComputation(): Int = withContext(Dispatchers.Default) {
// CPU 密集型計算
var result = 0
repeat(1_000_000) {
result += it
}
result
}

fun main() = runBlocking {
val result = heavyComputation()
println("計算結果: $result")
}

🎯 實戰範例

依層級使用 Dispatcher

// Repository - IO
class UserRepository {
suspend fun fetchUser(id: String): User = withContext(Dispatchers.IO) {
delay(1000) // 網路請求
User(id, "洪吉童")
}
}

// UseCase - Default
class ProcessUserUseCase {
suspend fun process(user: User): ProcessedUser = withContext(Dispatchers.Default) {
// 資料處理
delay(500)
ProcessedUser(user.name.uppercase())
}
}

data class User(val id: String, val name: String)
data class ProcessedUser(val displayName: String)

fun main() = runBlocking {
val repo = UserRepository()
val useCase = ProcessUserUseCase()

val user = repo.fetchUser("123")
val processed = useCase.process(user)
println("結果: ${processed.displayName}")
}

並行 IO 操作

suspend fun loadAllData(): Triple<String, String, String> = coroutineScope {
val user = async(Dispatchers.IO) {
delay(1000)
"使用者資料"
}

val posts = async(Dispatchers.IO) {
delay(1500)
"文章資料"
}

val comments = async(Dispatchers.IO) {
delay(800)
"留言資料"
}

Triple(user.await(), posts.await(), comments.await())
}

fun main() = runBlocking {
val time = measureTimeMillis {
val (user, posts, comments) = loadAllData()
println("$user, $posts, $comments")
}
println("耗時: ${time}ms") // ~1500ms (並行)
}

🔧 組合 Context

命名

fun main() = runBlocking {
launch(CoroutineName("任務1")) {
println("名稱: ${coroutineContext[CoroutineName]}")
}

launch(Dispatchers.IO + CoroutineName("IO任務")) {
println("執行緒: ${Thread.currentThread().name}")
println("名稱: ${coroutineContext[CoroutineName]}")
}

delay(100)
}

新增 Job

fun main() = runBlocking {
val job = Job()

launch(job + Dispatchers.Default) {
println("執行任務")
delay(1000)
println("完成任務")
}

delay(500)
println("取消任務")
job.cancel()
}

🎨 withContext

切換執行緒

suspend fun complexTask() {
println("開始: ${Thread.currentThread().name}")

// IO 操作
val data = withContext(Dispatchers.IO) {
println("IO: ${Thread.currentThread().name}")
"資料"
}

// CPU 操作
val processed = withContext(Dispatchers.Default) {
println("Default: ${Thread.currentThread().name}")
data.uppercase()
}

println("結束: ${Thread.currentThread().name}")
println("結果: $processed")
}

fun main() = runBlocking {
complexTask()
}

優化模式

// ❌ 不必要的切換
suspend fun bad() {
withContext(Dispatchers.IO) {
val data1 = loadData1()
withContext(Dispatchers.Default) { // 不必要!
process(data1)
}
}
}

// ✅ 高效
suspend fun good() {
val data1 = withContext(Dispatchers.IO) {
loadData1()
}

withContext(Dispatchers.Default) {
process(data1)
}
}

suspend fun loadData1() = delay(100)
suspend fun process(data: Unit) = delay(100)

🔥 實用模式

快取 + 網路

class DataSource {
private var cache: String? = null

suspend fun getData(): String {
// 檢查快取(快速)
cache?.let { return it }

// 網路請求(慢速)
return withContext(Dispatchers.IO) {
delay(1000)
"新資料"
}.also { cache = it }
}
}

fun main() = runBlocking {
val source = DataSource()

// 第一次呼叫 - 網路
val time1 = measureTimeMillis {
println(source.getData())
}
println("第一次呼叫: ${time1}ms")

// 第二次 - 快取
val time2 = measureTimeMillis {
println(source.getData())
}
println("第二次: ${time2}ms")
}

批次處理

suspend fun processBatch(items: List<Int>): List<Int> {
return withContext(Dispatchers.Default) {
items.map { item ->
// 處理每個項目
item * 2
}
}
}

fun main() = runBlocking {
val items = List(100) { it }
val results = processBatch(items)
println("處理完成: ${results.size}個")
}

搭配逾時處理

suspend fun fetchWithTimeout(): String? {
return try {
withTimeout(2000) {
withContext(Dispatchers.IO) {
delay(3000) // 花費太長時間
"資料"
}
}
} catch (e: TimeoutCancellationException) {
null
}
}

fun main() = runBlocking {
val result = fetchWithTimeout()
println("結果: ${result ?: "逾時"}")
}

🛡️ 例外處理

CoroutineExceptionHandler

fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("錯誤處理: ${exception.message}")
}

val job = launch(handler) {
throw Exception("發生問題!")
}

job.join()
println("繼續執行")
}

SupervisorJob

fun main() = runBlocking {
val supervisor = SupervisorJob()

with(CoroutineScope(coroutineContext + supervisor)) {
val job1 = launch {
delay(500)
throw Exception("任務1 失敗")
}

val job2 = launch {
delay(1000)
println("任務2 成功!")
}

try {
job1.join()
} catch (e: Exception) {
println("任務1 例外: ${e.message}")
}

job2.join()
}
}

🎯 自訂 Dispatcher

指定執行緒池大小

fun main() = runBlocking {
val customDispatcher = Dispatchers.IO.limitedParallelism(2)

repeat(5) { i ->
launch(customDispatcher) {
println("任務 $i: ${Thread.currentThread().name}")
delay(1000)
}
}

delay(3000)
}

單一執行緒

fun main() = runBlocking {
val singleThread = Dispatchers.Default.limitedParallelism(1)

repeat(3) { i ->
launch(singleThread) {
println("任務 $i: ${Thread.currentThread().name}")
delay(500)
}
}

delay(2000)
}

🤔 常見問題

Q1. 應該使用哪個 Dispatcher?

A: 根據任務類型選擇!

// IO - 網路、檔案、資料庫
suspend fun fetchData() = withContext(Dispatchers.IO) { }

// Default - CPU 密集型計算
suspend fun compute() = withContext(Dispatchers.Default) { }

// Main - UI 更新 (Android/Desktop)
suspend fun updateUI() = withContext(Dispatchers.Main) { }

Q2. 可以多次使用 withContext 嗎?

A: 可以!需要時就切換。

suspend fun workflow() {
val data = withContext(Dispatchers.IO) {
loadFromNetwork()
}

val processed = withContext(Dispatchers.Default) {
processData(data)
}

withContext(Dispatchers.Main) {
updateUI(processed)
}
}

Q3. 如果不指定 Dispatcher 會怎樣?

A: 會繼承父協程的 Context!

fun main() = runBlocking(Dispatchers.Default) {
launch { // 繼承 Dispatchers.Default
println(Thread.currentThread().name)
}
}

🎬 總結

使用 Context 和 Dispatcher 來控制協程!

核心整理:
✅ Dispatchers.IO - 網路/檔案
✅ Dispatchers.Default - CPU 操作
✅ Dispatchers.Main - UI 更新
✅ 使用 withContext 切換執行緒
✅ 組合 Context 進行精細控制

恭喜!您已完成 Coroutines 系列! 🎉

下一步: 在單元測試中學習如何撰寫測試!