show code block

2023年11月1日 星期三

協程(coroutine) - suspend、Dispatchers、Scope.launch (初略介紹)

Kotlin 協程是一個強大的同時也相對輕量級的工具,可以使異步編程變得更加容易和直觀。


基本概念:


協程: 是輕量級的執行緒,它們在某些情境下可以被暫停並在稍後恢復。
挂起函数 (Suspend Functions): 可以暫停當前的協程而不阻塞執行緒。它們使用 suspend 關鍵字。
使用 launch 和 async 建立協程:

launch 是最常見的啟動協程的方式,它返回一個 Job。
async 用於啟動一個協程並返回一個 Deferred<T>,通常用於異步計算結果。
協程上下文和調度器 (Dispatchers):

協程

總是在某個上下文中運行,最常見的調度器有 Dispatchers.Main, Dispatchers.IO, 和 Dispatchers.Default。

withContext: 在不同的調度器或上下文中切換協程的工作。

組合協程: 使用 join 和 await 來等待協程完成。

錯誤處理: 學習如何使用 try/catch 捕獲協程中的異常以及如何設置協程的異常處理程序。

Flow: Kotlin 提供的一種冷數據流工具,專為協程設計。與 RxJava 相似,但更為簡潔。

協程測試: 使用 runBlockingTest 和其他工具來測試協程。

實際應用: 在實際項目中使用協程,如 Android 的 viewModelScope。

進階主題:

協程共享狀態和同步。
通道 (Channels) 和 Actor 模型。
學習資源:

建議開始時可以從 Kotlin 官方文檔開始,然後進入到 Android 的協程使用文檔。隨著練習和深入學習,你會更加熟悉協程的概念和用法。


先來了解要如何使用它

Scope.launch


  CoroutineScope(Dispatchers.Main).launch {
    // 在主線程上執行的協程代碼
    val data = withContext(Dispatchers.IO) {
        // 切換到 I/O 線程,執行阻塞性 I/O 任務
        loadDataFromDisk()
    }
    // 回到主線程,更新 UI
    updateUI(data)
}
  

當你使用 Scope.launch { ... } 這種結構時,你正在在指定的協程範圍(Scope)內啟動一個新的協程。讓我們分步驟來詳細解釋這個過程:

1. 協程範圍(CoroutineScope)

每個協程都運行在某個 CoroutineScope 內,這個範圍決定了協程的生命周期。協程會隨著它的範圍被取消而取消。
範圍可以代表應用的結構單位,比如一個 Android 的 Activity 或 ViewModel。例如,viewModelScope 是綁定到 ViewModel 生命周期的一個範圍,在 ViewModel 被清理時,所有在這個範圍中的協程都會自動取消。

2. launch 函數

launch 是一個擴展函數,用於在協程範圍內啟動新協程。它立即返回一個 Job 對象,這個對象代表了協程。
launch 不會阻塞當前線程,並且它內部的協程代碼會在協程調度器決定的線程上異步執行。

3. 協程的生命周期

協程的生命周期與它的範圍(CoroutineScope)緊密相連。當範圍被取消時,所有在該範圍內的協程都會被取消。
如果協程內發生未捕獲的異常,默認情況下它會導致整個範圍的取消,這樣範圍內的其他協程也會被取消。

4. 調度器(Dispatcher)

launch 可以接受一個可選的 CoroutineContext 參數,通常是一個 Dispatcher。這個 Dispatcher 決定了協程應該在哪個線程或線程池上執行。
如果沒有指定調度器,協程將繼承它的範圍的調度器。例如,在 viewModelScope.launch { ... } 中,如果沒有指定調度器,那麼 viewModelScope 的默認調度器將被使用。




*Dispatchers:

在 Kotlin 協程中,Dispatchers 是用來指定協程應該在哪種類型的線程上執行的。Dispatchers 可以決定協程的行為和性能特性,如何選擇合適的 Dispatcher 對於寫出高效和響應式的應用程序非常重要。

Kotlin 協程中提供了幾個核心的 Dispatcher:

1. Dispatchers.Main

用於 Android 的主線程,用於更新 UI 和處理與 UI 相關的任務。
任何在這個調度器上啟動的協程都會在 Android 的主線程上執行。
不應該用於執行耗時或阻塞性的操作,因為這會導致 UI 卡頓。

2. Dispatchers.IO

專為 I/O 任務(如網絡請求、讀取和寫入文件)而設計。
擁有一個可擴展的線程池,可以根據需要增加線程數量。
適合用於執行可能會阻塞線程的 I/O 任務。

3. Dispatchers.Default

專為 CPU 密集型工作,如大規模的數據處理和計算任務。
使用一個固定大小的線程池,大小預設與處理器的核心數量相同。
不適合 I/O 任務,因為那會浪費有限的計算資源。

4. Dispatchers.Unconfined

這個調度器將協程執行在當前的線程上,但只在第一次暫停點之前。暫停後,它會在恢復協程的那個線程上繼續執行。
它不提供任何確定性的線程模型。通常不推薦使用,除非你非常清楚為什麼需要它,因為它可能會導致一些難以發現的錯誤。

5. 自定義 Dispatcher

可以通過使用 Executor 來創建自定義的 CoroutineDispatcher。
這可以讓你根據應用的特定需求來定制線程的行為。

通常比較會使用到的就只有前三個
// 使用 Main 調度器,在主線程上執行
GlobalScope.launch(Dispatchers.Main) {
    // 更新 UI
}

// 使用 IO 調度器,進行 I/O 任務
GlobalScope.launch(Dispatchers.IO) {
    // 執行 I/O 操作
}

// 使用 Default 調度器,進行計算任務
GlobalScope.launch(Dispatchers.Default) {
    // 執行耗時的計算
}

// 使用 Unconfined 調度器
GlobalScope.launch(Dispatchers.Unconfined) {
    // 慎用,協程可以在任何線程上恢復
}

// 創建自定義的 Dispatcher
val myDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
GlobalScope.launch(myDispatcher) {
    // 在自定義的線程上執行
}





*挂起函數(Suspending Functions) -  suspend

他是coroutine協程的核心概念之一

使用場景:
掛起函數通常用於執行耗時的任務
如:
網絡通信:發送請求並等待響應。
數據庫操作:執行查詢並等待結果。
文件處理:讀寫大文件操作。
任何可能耗時的操作,你不想在主線程上執行。

工作原理:
當呼叫一個挂起函數時,如果該函數需要等待(例如,等待網絡響應),它會挂起包含它的協程,而不是阻塞線程。等到挂起的操作完成時,協程的執行將恢復。

特點:
只能被其他掛起函數或協程的建構器內呼叫。
可以調用其他掛起函數。
不會阻塞當前線程,從而允許其他任務在同一個線程上運行。

使用方式:
他的使用方式很簡單
只需要在fun前面加入suspend就可以宣告成掛起函數
suspend fun fetchDoc(id: String): Document {
    // 模擬耗時操作
    delay(1000)
    return Document(id)
}

這樣就變成一個掛起函數了
但並不是只要加了suspend他就會變成不阻塞主線程的function
必須配合

  //..在activity內使用
  lifecycleScope.launch {
  
            // 在這裡執行你的異步操作
         val doc = fetchDoc("123") // 調用掛起函數
         println(doc)
         
        }

而你想調用suspend function必須也是suspend function
這樣就就是一個簡單的掛起函數使用方式

來講一個我們最常用的使用情境
call api的情況
   
   // Repository中
suspend fun fetchUserData(): UserData {

return withContext(Dispatchers.IO) { // 切換到IO調度器
// 執行網絡請求,並等待結果
apiService.getUserData()

}
}

  // ViewModel中
fun fetchUserData() {

viewModelScope.launch { // 在ViewModel作用域內啟動協程

val userData = repository.fetchUserData() // 調用掛起函數獲取數據
userLiveData.postValue(userData) // 更新LiveData

}

}


  
上面範例中使用到了withContext(Dispatchers.IO),你告訴協程應該在一個專門用於 I/O 任務的線程池中運行,意思也就是非主線程。
什麼是IO?
I/O 是 Input/Output 的縮寫,即“輸入/輸出”。
後面會再詳述

Scope.launch的用法意思就是在你想要創建的線程上,創建一個協程Coroutine
就好比你在主線程上創建了協程,代表什麼呢?

想像主線程像是一間餐廳的主廚,他需要處理各種事情:烹飪、盤點庫存、接電話訂單等。
這間餐廳就是你的應用,而顧客希望服務快速且不出錯。

當你使用scope.launch時,就像主廚叫了一個助手來處理一項特定的任務,比如煮一鍋湯。
這個助手在同一個廚房中(主線程上)工作,但是他有自己的工作空間,不會干擾主廚做其他事情。這就是協程,它運行在主線程中但是不阻塞主廚的其他工作。

suspend函數就像是這個助手在煮湯時需要等待湯煮開。助手不會站在爐子旁邊發呆等湯煮開,他會做其他事情,比如切菜,直到湯煮開了他再回來繼續這項任務。

這就是掛起的概念:暫時將某個需要等待的任務放到一邊,直到可以繼續進行為止。

所以,當你在使用協程時,你基本上是在告訴主廚:「這裡有一些任務可以同時進行,但你不需要時刻盯著它們,只需要確保在必要的時候回來處理即可。」這樣主廚(主線程)的時間就被利用得更高效了,顧客(用戶)也會因為服務迅速而滿意。













沒有留言:

張貼留言

協程(coroutine) - 協程為什麼要學它?

 Coroutine 協程 再強調一次 協程就是由kotlin官方所提供的線程api //Thread Thread { }.start() //Executor val execu...