Kotlin協程實際上是所謂的stackless協程,即沒有在線程之上實現類似線程棧的結構,可以認為是一種kotlin語言層面支持的 線程調度框架,使用這個框架,我們可以省去手動書寫callback,使代碼看上去是同步的
使用了協程以后,原本需要回調實現的非阻塞功能可以用阻塞的方式去寫,比如:
launch {
// 異步讀時掛起
val bytesRead = inChannel.aRead(buf)
// 讀完成后才執行這一行
...
...
process(buf, bytesRead)
// 異步寫時掛起
outChannel.aWrite(buf)
// 寫完成后才執行這一行
...
...
outFile.close()
}
// 協程掛起后,線程繼續執行
println("thread continue")
執行上述代碼段時,在協程運行到val bytesRead = inChannel.aRead(buf)
這一句時掛起,線程繼續執行協程外的代碼,輸出thread continue
那么,協程是如何做到這樣的“黑科技”呢?網上搜一圈,基本上都會搜到CPS(Continuation-Passing-Style, 續體傳遞風格),聽起來挺玄乎的,什么是CPS呢?
說的簡單點,其實就是函數通過回調傳遞結果,讓我們看看這個例子
class Test {
public static long plus(int i1, int i2) {
return i1 + i2;
}
public static void main(String[] args) {
System.out.println(plus(1, 2));
}
}
這個例子是常規的寫法,函數plus的結果通過函數返回值的形式返回并進行后續處理(這里僅僅打印),如果把例子改寫成CPS風格,則是
class Test {
interface Continuation {
void next(int result);
}
public static void plus(int i1, int i2, Continuation continuation) {
continuation.next(i1 + i2);
}
public static void main(String[] args) {
plus(1, 2, result -> System.out.println(result));
}
}
很簡單吧?這就是CPS風格,函數的結果通過回調來傳遞
協程里通過在CPS的Continuation回調里結合狀態機流轉,來實現協程掛起-恢復的功能,來看下面的例子
假設我們有一個擴展的掛起函數:
suspend fun <T> CompletableFuture<T>.await(): T
在編譯過后,其函數簽名將變成
fun <T> CompletableFuture<T>.await(continuation: Continuation<T>): Any?
可以看到,入口參數多了一個Continuation類型,這個就是CPS續體,其實也就是我們上面說的回調
現在我們再假設有這么段代碼
val a = a()
val y = foo(a).await() // 掛起點 #1
b()
val z = bar(a, y).await() // 掛起點 #2
c(z)
這段代碼必須在協程里面執行(launch{}
或者async{}
等的lambda里),且調用了兩個await()
掛起函數,其編譯后將變成這樣的偽代碼(因為是編譯器直接生成字節碼)
class <anonymous_for_state_machine> extends SuspendLambda<...> {
// 狀態機當前狀態
int label = 0
// 協程的局部變量
A a = null
Y y = null
void resumeWith(Object result) {
if (label == 0) goto L0
if (label == 1) goto L1
if (label == 2) goto L2
else throw IllegalStateException()
L0:
// 這次調用,result 應該為空
a = a()
label = 1
result = foo(a).await(this) // 'this' 作為續體傳遞
if (result == COROUTINE_SUSPENDED) return // 如果 await 掛起了執行則返回
L1:
// 外部代碼傳入 .await() 的結果恢復協程
y = (Y) result
b()
label = 2
result = bar(a, y).await(this) // 'this' 作為續體傳遞
if (result == COROUTINE_SUSPENDED) return // 如果 await 掛起了執行則返回
L2:
// 外部代碼傳入 .await() 的結果恢復協程
Z z = (Z) result
c(z)
label = -1 // 沒有其他步驟了
return
}
}
可以看到,以兩個掛起函數的調用點為分界,生成了一個具有3個狀態的狀態機類
現在,當協程開始時,我們調用了它的 resumeWith() —— label 是 0,然后我們跳去 L0,接著我們做一些工作,將 label 設為下一個狀態—— 1,調用 .await(),如果協程執行掛起就返回。當我們想繼續執行時,我們再次調用 resumeWith(),現在它繼續執行到了 L1,做一些工作,將狀態設為 2,調用 .await(),同樣在掛起時返回。下一次它從 L3 繼續,將狀態設為 -1,這意味著"結束了,沒有更多工作要做了"。
其實編譯后的代碼就是利用這個生成的類作為續體(Continuation)傳遞了掛起點前后的中間結果,并且通過狀態機,來記憶協程恢復后應該執行哪段代碼
好了,這就是協程掛起和恢復的實現方式了,可以看到,我們并不能把Kotlin協程當作是所謂的“輕量級線程”來解釋,它更像是一個以同步方式去寫異步方法,并幫助開發者生成回調方法(CPS風格)的線程調度框架,當然本文只分析了協程的掛起和恢復,還有協程的取消操作,上下文(可用于限定協程在某個線程工作)等等功能,原理解析推薦閱讀:
Kotlin中文官網的協程設計文檔(中文)
Bennyhuo的破解Kotlin協程系列(中文)
協程的使用推薦閱讀:
扔物線的Kotlin教學視頻及文章(協程部分)
google codelabs kotlin coroutines guide(手把手教你寫協程)