kotlin協程實現原理梳理

我們接觸協程,往往會有如下疑問,本文一一解答

  1. 異步是怎么實現的,即執行權是怎么轉移的?
  2. 掛起函數執行完畢后是怎么恢復現場,繼續執行后續代碼的?
  3. 協程里面各部分代碼都在哪個線程上執行?

一、協程的簡單使用示例

注意看注釋,各部分代碼在哪個線程上執行

// 使用調度器啟動一個協程
launch(Dispatchers.IO) {
    // 這個代碼塊會在 dispatcher 的線程池中的一個線程上執行,假定是A
    print("A")

    // 調用一個掛起函數,如果內部實現沒有切換執行線程,將仍舊在A線程上執行
    suspendFunction()

    // 當 suspendFunction 完成后,這個代碼塊會盡量在原來的A線程上恢復執行,但是有可能會是別的IO線程
    print("B")
}

二、 協程的幾個關鍵對象

1. CoroutineScope接口

定義了協程的作用域,是生命周期管理的關鍵類,包含CoroutineContext

public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

2. CoroutineContext

協程執行的上下文,上面提供了很多工具方法,比如包含一個調度器

3. 協程Coroutine

理解為一個任務,是job接口的一種實現

private open class StandaloneCoroutine(
    parentContext: CoroutineContext,
    active: Boolean
) : AbstractCoroutine<Unit>(parentContext, initParentJob = true, active = active) {
    override fun handleJobException(exception: Throwable): Boolean {
        handleCoroutineException(context, exception)
        return true
    }
}

4. 調度器CoroutineDispatcher

決定了協程任務在哪個線程上運行, 簡化代碼如下:

public abstract class CoroutineDispatcher {
     //分發任務到特定線程
     public abstract fun dispatch(context: CoroutineContext, block: Runnable)
}
  • Dispatchers.Main:這個調度器用于在主線程中執行協程。這通常用于更新 UI 或者執行其他需要在主線程中執行的任務。如果你嘗試在沒有主線程的環境中使用它,比如后端應用,它會拋出異常。
  • Dispatchers.IO:這個調度器用于執行 I/O 密集型任務,比如網絡請求或者讀寫文件。它內部使用了一個用于 I/O 任務的線程池。
  • Dispatchers.Default:這個調度器用于執行 CPU 密集型任務,比如復雜的計算或者排序操作。它內部使用了一個用于計算的線程池。
  • Dispatchers.Unconfined:這個調度器有一個特殊的行為,協程會在調用它的線程立即執行,直到第一個掛起點。當協程被喚醒時,它會在喚醒它的線程繼續執行

5. 協程創建器:launch, async等

可以看到創建器被定義成CoroutineScope的擴展函數

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
    LazyStandaloneCoroutine(newContext, block) else
    StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine    
}

6. 掛起函數

不用多說,異步任務可以定義成掛起函數

三、協程實現原理展示

kotlin的很多特性都是用過編譯器動態修改代碼來實現,協程的實現原理也是一樣,他通過把協程轉換為一種狀態機來轉讓執行權和恢復原來執行代碼。

我們用一個簡化形式的代碼來理解這一點,注意看注釋。

我們假定寫了如下代碼:

launch(Dispatchers.IO) { 
   print("A") 
   //這是一個掛起函數
   doSomething() 
   print("B") 
 } 

上述代碼會被kotlin轉化為:

//狀態機類,很多文檔也翻譯成連續體
interface Continuation<T> {
    val context: CoroutineDispatcher fun resumeWith(result: Result<T>)
 }


//創建狀態機類
val coroutine = object : Continuation<Unit> { 
    var label = 0 
    val coroutineDispatcher = XXX
    override fun resumeWith(result: Result<Unit>) { 
        //使用調度器來把任務分發到特定線程!!!
        coroutineDispatcher.dispatch(Runnable { 
          when (label) {
             0 -> { 
                print("A") 
                label = 1 
                doSomething(this) //注意:掛起函數被傳入了額外參數,就是Continuation實例!!!
                } 
             1 -> { 
                print("B")  
                    } 
            } 
        } 
    } 
} 


fun doSomething(Continuation c){
    //原來異步邏輯....省略
    
    //執行完畢后調用連續體,恢復原來的執行流程
    c.resumeWith(XXX)
}

//啟動協程 
coroutine.resumeWith(Result.success(Unit))

上述流程概括起來為3步:

  • 在協程在編譯的時候會被轉化為一個狀態機,實現Continuation接口,
  • 掛起函數后面的代碼內容會被塞入狀態機的下一個狀態分支
  • Continuation實現類會被當做額外參數,傳遞給原來的掛起函數,掛起函數執行完畢后會繼續調用Continuation.resumeWith()方法恢復執行
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容