我們接觸協程,往往會有如下疑問,本文一一解答
- 異步是怎么實現的,即執行權是怎么轉移的?
- 掛起函數執行完畢后是怎么恢復現場,繼續執行后續代碼的?
- 協程里面各部分代碼都在哪個線程上執行?
一、協程的簡單使用示例
注意看注釋,各部分代碼在哪個線程上執行
// 使用調度器啟動一個協程
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()方法恢復執行