show code block

2023年11月29日 星期三

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

 Coroutine 協程

再強調一次

協程就是由kotlin官方所提供的線程api

  
        //Thread
        Thread {

        }.start()

        //Executor
        val executor = Executors.newCachedThreadPool()
        executor.execute {

        }

        //coroutine
        launch {

        }
  
  
Rxjava都有了 coroutine的優勢在哪?
本質上跟其他的線程api一樣,方便
他借助了kotlin的語言優勢,所以他基於那些Java之上的方案會更方便一點
最重要的是
他可以用看起來同步的方式,寫出異步代碼

  
   
        val user = api.getUser()//後台線程
        tvName.text = user.name //主線程
  
  

這就是kotlin最有名的『非阻塞式掛起』suspend
Coroutine最大的好處,是你可以把不同線程的代碼寫在同一塊代碼塊裡面
coroutine:
  
   
        launch(Dispatchers.Main) { // (主線程)
        
            val result = api.fetchData() // 異步操作,比如獲取網絡數據(後台線程)
            tvName.text = result.name // 在這裡處理結果,如更新UI(主線程)
            //上下兩行代碼,可以先切走在切回來,這是java完全做不到的事情
            
        }

  
正常callback寫法:
  
   
        api.getUser()
        .enqueue(object:Callback<User>{
        ...
        override fun onResponse(call:Call<User>,
                                response:Response<User>){
        
          runOnUiThread{
          
          tvName.text = response.body().name
          
          }
                               
         }
        
        })

  
Callback的寫法我們早就爛到骨頭裡了,你為什麼還是要學習Coroutine?
省了超多callback代碼,超簡潔 
消除了callback 多線程的操作難度直接抹平了 
不是10 -> 9 
而是 1 -> 0

我舉一個差異最大的例子
像是我要做一個
兩次網路請求
並把這兩個輸出結果拿去做第三個網路請求

callback式的開發,要做這種工作非常困難
於是我們可能會選擇妥協
變成完成a後再call b的先後請求,這就是標準的垃圾代碼了
明明可以並行的兩個請求,由於我自身能力不足
我讓網路時間等待長了一倍,也就是性能上差了一倍

如果是使用協程
  
launch(Dispatchers.Main) {

    val userDetails = async {

        api.getUserDetails(user) // 獲取用戶詳細資料

    }

    val userPosts = async {

        api.getUserPosts(user) // 獲取用戶發布的帖子

    }

    val combinedData = suspendingMerge(userDetails, userPosts) // 合併資料
    // 接下來可以處理合併後的資料

}
 
coroutine由於消除了併發之間協作的難度
可以輕鬆地寫出複雜的併發代碼
甚至有些不可能實現的併發任務,變成可能甚至變得很簡單
這些才是協程的優勢所在

但這時你可能會有個疑惑
你要在後台執行任務?切協程
要在主線程執行任務?切協程
  
 
          launch(Dispatchers.IO) {
          
              val fileData = readFile(fileId) // 在IO線程中讀取文件數據

              launch(Dispatchers.Main) {
                  fileTextView.setText(fileData) // 在主線程中更新文本視圖
              }
              
          }

   
 這不也是callback地獄?
 讓code很髒很不簡潔?
這邊就要講到
coroutine有一個很厲害的函數 withContext
  
          launch(Dispatchers.Main) {
              val fileContent = withContext(Dispatchers.IO) {
                  readFile(fileId) // 在IO線程中讀取文件內容
              }

              fileTextView.setText(fileContent) // 在主線程中設置文本視圖的內容
          }
 
可以寫成這樣,看起來區別不大
但如果你有更多的線程切換,優勢就體現出來了

由於有自動切回的功能,Coroutine消除了併發代碼在協作時的嵌套
 
        launch(Dispatchers.IO){
            ...
            launch(Dispatchers.Main){
                ...
                launch(Dispatchers.IO){
                    ...
                    launch(Dispatchers.Main){
                    }
                }
            }
        }
   
由於有了『自動切回來的功能』
寫成消除了在併發代碼協作時的嵌套
直接寫成上下關係代碼
就能讓Coroutine之間進行協作
 
        launch(Dispatchers.Main){
            withContext(Dispatchers.IO){
                ...
            }
                ...
            withContext(Dispatchers.IO){
                ...
            }
                ...
        }
     
這就是協程 
『協作式的例程』

而且由於消除了嵌套
你可以把withContext放到函數裡面
用她包著函數的實際業務代碼
但function要在前方用掛起函數
 
        launch(Dispatchers.Main) {
        
            val fileContent = suspendingReadFile(fileId)
            fileTextView.setText(fileContent)
            
        }

        suspend fun suspendingReadFile(fileId: String): String {
        
            return withContext(Dispatchers.IO) {
                readFile(fileId) // 在IO線程中讀取文件
            }
            
        }

     
suspend是coroutine最核心的關鍵詞
他也是最難懂,最多被誤解的點
『suspend是掛起,非阻塞式的,他並不會阻擋你的線程』
這你看得懂嗎? 我一開始是看不懂

再說一次
協程是什麼?
一個線程框架

好在哪?好在方便
最方便的地方在哪?
他可以在同一個代碼塊中做線程切換














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...