掘金遷移地址:【譯文】扒一扒Kotlin Coroutines幕后實現
原文地址: Kotlin Coroutines 幕後那一兩件事
前言
如果你能看完本文并把所有內容都弄懂,你對協(xié)程的理解也已經超過大部分人了。
Coroutines是近幾年在Kotlin上Google主推的異步問題解決方案,至少在Android R Asynctask被放棄后,打開Android Document看到最顯目的提示項目就是導引你至Coroutine的頁面教導你怎么使用Coroutine。
Emm….那如果把所有問題簡單化,其實大多數碰上異步問題,解決的辦法基本上都是callback。
fun longTimeProcess(parameter1 : String, callBack:CallBack<String>){
val result = ""
//Do something long time
callBack.onResponse(result)
}
其實我們一般執(zhí)行回調,本質上都是callback
,所以很多異步解決方案,追根溯源,會發(fā)現他的實現方式仍舊是callback。
不過callback的使用情境、context還有許許多多的用法情況都不同,整體概念也會有出入,所以我們會需要更多的名詞來代表這樣的情況,因此延伸出更多樣的詞匯,不過這段就題外話了。
話說回來,上面那段簡易的callback,換作是Coroutine會變成是這樣:
suspend fun longTimeProcess(parameter1:String):String{
val result =“”
//Do something long time
return result
}
這樣寫的好處是可以不用自己控制Thread的使用,上面的代碼如果直接在主線程執(zhí)行,可能會造成主線程卡頓,超過5秒噴Exception直接讓Process out,所以還會需要額外自己開thread + handler
或是使用Rxjava之類第三方套件去處理。換作是Coroutine,使用起來就簡單很多了,被suspend
修飾的函數longTimeProcess,有自己的作用域(Scope),用scope launch
里頭執(zhí)行該function,利用這個function回傳的數據做該在main thread上解決的事情,問題解決,就是如此的簡單。
那問題來了。
Coroutine到底是怎么運作的?究竟是甚么神奇的魔法讓他可以這么的方便可以不用寫那么多東西呢?
記得某次面試里有提到這個問題,但我只知道他是個有限狀態(tài)機,然后就…
恩,我那時的表情應該跟King Crimson有那么幾分神似就是了。
Coroutine概念
維基百科上其實有解釋了Coroutine的實作概念:
var q := new queue
coroutine produce
loop
while q is not full
create some new items
add the items to q
yield to consume
coroutine consume
loop
while q is not empty
remove some items from q
use the items
yield to produce
概念是,這有個queue是空的,那是先跑coroutine product還是coroutine consume其實無所謂,總之隨便跑一個,先從coroutine product開始好了。
coroutine produce在queue沒滿時,會產生一些items,然后加入queue里頭,直到queue滿為止,接著把程序讓給coroutine consume。
coroutine consume在queue不是空的時候,會移除(消費)一些items,直到queue空為止,接著把程序讓給coroutine produce,如此反復,這個世界的經濟得以維持。
那這邊可以看出,當coroutine produce
碰到queue是滿的時候會直接把程序讓給coroutine consume
;相對的,若coroutine consume
在碰到queue是空的時候,會直接把程序讓給coroutine produce
。
那么,以Kotlin Coroutine來說,queue的是空是滿的條件會變成是method的狀態(tài)是否suspend,那因為上面這個程序很明顯會是無限循環(huán),多數我們在開發(fā)時會不需要無限的循環(huán),那怎么樣才能讓這種來回傳接球的形式有個終點呢?
答案就是有限狀態(tài)機,接下來這篇文章會慢慢地解釋。
有這么個東西叫做 Continuation
很多時候,原本很麻煩的事情突然變得簡單了,其實不是什么都不用做,而是事情有人幫你做了,Coroutine也是,它幫你把寫一堆callback
的麻煩事給做掉了。
等等,Compiler把寫一堆的callback的麻煩事給做掉了,那意思是…
沒錯,Coroutine本質上還是callback
,只是編譯器幫你寫了。
我本來是想說從CoroutineScope.Launch
下去追的,追到IntrinsicsJvm
,這東西叫Intrinsic這東西有很大的機率是給編譯器用的,追到這里,大概就可以知道,suspend fun
會在編譯的過程轉成Continuation
.
但后來換個方向去想,其實也不用這么麻煩,因為Kotlin是可以給Java呼叫的,那Java比較少這種語法糖轉譯的東西,也就是說,透過Java呼叫suspend fun,就可以知道suspend fun真正的模樣。
這邊先隨便寫一個suspend fun。
suspend fun getUserDescription(name:String,id:Int):String{
return ""
}
在 Java 中調用的時候是如下這樣:
instance.getUserDescription("name", 0, new Continuation<String>() {
@NotNull
@Override
public CoroutineContext getContext() {
return null;
}
@Override
public void resumeWith(@NotNull Object o) {
}
});
return 0;
我們可以看到,其實suspend fun
就是一般的function后頭加上一個Continuation
。
總之得到一個線索,這個線索就是Continuation,它是個什么玩意呢?
它是一個 interface
public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>)
}
它代表的是Coroutine
的runBlock
在suspend
狀態(tài)中,要被喚醒的callback
。
那注意這邊提到狀態(tài)了,大伙都知道Coroutine會是個狀態(tài)機,那具體是怎么個狀態(tài)呢?這個稍后提。
那如果硬要在java file里頭使用GlobalScope.launch,那會長成這樣:
BuildersKt.launch(GlobalScope.INSTANCE,
Dispatchers.getMain(),//context to be ran on
CoroutineStart.DEFAULT,
new Function2<CoroutineScope,Continuation<? super Unit>,String>() {
@Override
public String invoke(CoroutineScope coroutineScope, Continuation<? super Unit> continuation) {
return "";
}
}
);
這樣就行了嗎?這樣好像沒啥效果最后會回一個空字串就是了,但這里就會發(fā)現,如果用lanuch會需要用到一個Function去傳遞一個continuation。這樣看還是蒙,沒關系,咱們繼續(xù)看下去。
Continuation到底怎么運行?
那這邊簡單用一個suspend:
fun main() {
GlobalScope.launch {
val text = suspendFunction("text")
println(text) // print after delay
}
}
suspend fun suspendFunction(text:String) = withContext(Dispatchers.IO){
val result = doSomethingLongTimeProcess(text)
result
}
用 Kotlin Bytecode
去 decompile
會得到這個:
public static final void main() {
BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
private CoroutineScope p$;
Object L$0;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
Object var10000;
CoroutineScope $this$launch;
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
$this$launch = this.p$;
this.L$0 = $this$launch;
this.label = 1;
var10000 = CoroutineTestKt.suspendFunction("text", this);
if (var10000 == var5) {
return var5;
}
break;
case 1:
$this$launch = (CoroutineScope)this.L$0;
ResultKt.throwOnFailure($result);
var10000 = $result;
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
String text = (String)var10000;
boolean var4 = false;
System.out.println(text);
return Unit.INSTANCE;
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkParameterIsNotNull(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
var3.p$ = (CoroutineScope)value;
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);
}
另外一個是 suspendFunction 的 decompile code
:
public static final Object suspendFunction(@NotNull final String text, @NotNull Continuation $completion) {
return BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
private CoroutineScope p$;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
CoroutineScope $this$withContext = this.p$;
String result = CoroutineTestKt.doSomethingLongTimeProcess(text);
return result;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkParameterIsNotNull(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
var3.p$ = (CoroutineScope)value;
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), $completion);
}
字節(jié)碼反編譯成 Java 這種事,我們干過很多次了。跟往常不同的是,這次我不會直接貼反編譯后的代碼,因為如果我直接貼出反編譯后的 Java 代碼,估計會嚇退一大波人。協(xié)程反編譯后的代碼,邏輯實在是太繞了,可讀性實在太差了。沒關系,我們直接梳理解釋一下流程。
反編譯代碼中我們看到一個 switch(this.label) , 這就是大名鼎鼎的 Coroutine
的狀態(tài)機
了,Kotlin編譯器會在編譯時產生一個label
,這個label
就是runBlock
里邊執(zhí)行到第幾段的狀態(tài)了。
那具體會有幾個狀態(tài)呢?其實在runBlock
里邊有幾個suspend
就會對應有幾個狀態(tài)機,舉個例子:
GlobalScope.launch {
test()
test()
test()
test()
}
fun test(){}
如上代碼會有幾個呢?
答案是一個,因為這 test() 不是掛起函數(suspend function)
,它不需要掛起操作(suspended)。
如果換成是這樣?
GlobalScope.launch {
test()
test()
test()
test()
}
suspend fun test(){}
答案是五個。
GlobalScope.launch {
// case 0
test() // case 1 receive result
test() // case 2 receive result
test() // case 3 receive result
test() // case 4 receive result
}
因為四個 test()
都有可能獲得 suspended
的狀態(tài),所以需要五個執(zhí)行狀態(tài)的,case 0
用于初始化,case 1– 4
用于結果獲取。
那狀態(tài)何時會改變呢?
答案是:invokeSuspend
執(zhí)行時。
label34: {
label33: {
var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
$this$launch = this.p$;
this.L$0 = $this$launch;
this.label = 1;
if (CoroutineTestKt.test(this) == var3) {
return var3;
}
break;
case 1://...ignore
break;
case 2://...ignore
break label33;
case 3://...ignore
break label34;
case 4://...ignore
return Unit.INSTANCE;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
this.L$0 = $this$launch;
this.label = 2;
if (CoroutineTestKt.test(this) == var3) {
return var3;
}
}
this.L$0 = $this$launch;
this.label = 3;
if (CoroutineTestKt.test(this) == var3) {
return var3;
}
}
this.L$0 = $this$launch;
this.label = 4;
if (CoroutineTestKt.test(this) == var3) {
return var3;
} else {
return Unit.INSTANCE;
}
這部分比較有意思的地方是,這些狀態(tài)還有 call method
的都不在 switch case
里面,這其實跟 Bytecode
有關,主要是因為這個結果是 反編譯
出來的東西,所以會是這樣的疊加方式。
我們可以看到,在狀態(tài)機改變時:
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
//...ignore
this.label = 1;
if (CoroutineTestKt.test(this) == var3) {
return var3;
}
根據上述代碼可以看出, 編譯器內部有一個函數IntrinsicsKt.getCOROUTINE_SUSPENDED()
該函數代表當前的狀態(tài)是否掛起。如果它回傳的是 getCOROUTINE_SUSPENDED
,代表這個 function
處在 掛起(suspended)
的狀態(tài),意味著它可能當前正在進行耗時操作。這時候直接返回 掛起
狀態(tài),等待下一次被 調用(invoke)
。
那什么時候會再一次被 調用(invoke) 呢?
這時候就要看傳入到該掛起函數的的 Continuation
,這裡可以觀察一下 BaseContinuationImpl
的 resumeWith
的操作:
internal abstract class BaseContinuationImpl(
public val completion: Continuation<Any?>?
) : Continuation<Any?>, CoroutineStackFrame, Serializable {
public final override fun resumeWith(result: Result<Any?>) {
var current = this
var param = result
while (true) {
probeCoroutineResumed(current)
with(current) {
val completion = completion!!
val outcome: Result<Any?> =
try {
val outcome = invokeSuspend(param)
if (outcome === COROUTINE_SUSPENDED) return
Result.success(outcome)
} catch (exception: Throwable) {
Result.failure(exception)
}
releaseIntercepted()
if (completion is BaseContinuationImpl) {
current = completion
param = outcome
} else {
completion.resumeWith(outcome)
return
}
}
}
}
//...ignore
}
原則上 resumeWith
在一開始 Coroutine
被創(chuàng)建時就會執(zhí)行(所以需要 case 0 做初始化
),可以看到 invokeSuspend
會被執(zhí)行到。(probeCoroutineResumed 那個看起來是 debug 用的請無視
),通過執(zhí)行 invokeSuspend
開始執(zhí)行狀態(tài)機,如果該 continuation
的狀態(tài)是掛起,就會執(zhí)行return
,重新執(zhí)行 invokeSuspend
,等下一次被喚醒,再次被喚醒后,繼續(xù)執(zhí)行,直到得到結果,并且將結果通過 continuation((name in completion)
的 resumeWith
返回結果,結束此次執(zhí)行,接著繼續(xù)執(zhí)行掛起函數的的 invokeSuspend
,如此反復直至最終結束。
到這裡,我們知道了, 被
suspend標記的函數內部是通過狀態(tài)機才實現的掛起恢復的,并且利用狀態(tài)機來記錄Coroutine執(zhí)行的狀態(tài)
。
執(zhí)行掛起函數時可以得到它的狀態(tài)為:getCOROUTINE_SUSPENDED
。
不過又有問題來了,當掛起函數判斷條件為:getCOROUTINE_SUSPENDED
時執(zhí)行了 return
,代表它已經結束了,那它怎么能繼續(xù)執(zhí)行呢?而且還有辦法在執(zhí)行完后通知協(xié)程。
這里我們拿一段代碼來看看:
suspend fun suspendFunction(text:String) = withContext(Dispatchers.IO){
val result = doSomethingLongTimeProcess(text)
result //result 是個 String
}
它 decompile 後:
public static final Object suspendFunction(@NotNull final String text, @NotNull Continuation $completion) {
return BuildersKt.withContext(
(CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
//...ignore
}), $completion);
}
會發(fā)現,該函數 return
的不是 String
而是一個Object
,那這個Object
是什么呢?其實就是COROUTINE_SUSPENDED
。
要證明這點其實很簡單,如下代碼,調用該 suspendFunction
就可以了
Object text = instance.suspendFunction("", new Continuation<String>() {
@NotNull
@Override
public CoroutineContext getContext() {
return Dispatchers.getMain();
}
@Override
public void resumeWith(@NotNull Object o) {
}
});
System.out.println(text);
結果:
COROUTINE_SUSPENDED
Process finished with exit code 0
PS:如果該函數時一個普通函數,沒有標記
suspend
則會直接返回結果。
根據上邊我們這么多的分析,我們可以解釋那段代碼了。
fun main() {
GlobalScope.launch {
val text = suspendFunction("text")
println(text) // print after delay
}
}
suspend fun suspendFunction(text:String) = withContext(Dispatchers.IO){
val result = doSomethingLongTimeProcess(text)
result
}
首先,Kotlin編譯器會把 main()
里面的代碼反編譯生成一個Continuation,而 launch block
的部分生成一個有限的狀態(tài)機,并包裝進 Continuation
里面那個叫 invokeSuspend(result)
的方法里頭,并做為初次 resumeWith
。
Continuation { // GlobalScope.Lanuch()
var label = 0
fun invokeSuspend(result:Any):Any{
when(label){
0->{
val functionResult = suspendFunction("text",this)
lable = 1
if(functionResult == COROUTINE_SUSPENDED){
return functionResult
}
}
1->{
throwOnFailure(result)
break
}
}
val text = result as String
print(text)
}
}
invokeSuspend(result)
會在該 Continuation
的 resumeWith
執(zhí)行的時候執(zhí)行。
Continuation { // GlobalScope.Lanuch()
var label = 0
fun invokeSuspend(result:Any):Any{
when(label){
0->{
val functionResult = suspendFunction("text",this)
lable = 1
if(functionResult == COROUTINE_SUSPENDED){
return functionResult
}
}
1->{
throwOnFailure(result)
break
}
}
val text = result as String
print(text)
}
}
第一次執(zhí)行 invokeSuspend(result)
的時候,會執(zhí)行到 suspendFunction(String)
,并傳入包裝好的 Continuation
。
Continuation { // suspendFunction(text)
fun invokeSuspend(result:Any):Any{
when(label){
0->{
val text = doSomethingLongTimeProcess(context)
return 後執(zhí)行 continuation.resultWith(text)
}
}
}
}
suspendFunction
自己本身也是一個掛起函數,所以它也會包裝成一個 Continuation
(但這邊就單純很多,雖然也會生成狀態(tài)機,但其實就是直接跑doSomethingLongTimeProcess()
)。
Continuation { // GlobalScope.Lanuch()
var label = 0
fun invokeSuspend(result:Any):Any{
when(label){
0->{
val functionResult = suspendFunction("text",this)
lable = 1
if(functionResult == COROUTINE_SUSPENDED){
return functionResult
}
}
1->{
throwOnFailure(result)
break
}
}
val text = result as String
print(text)
}
}
因為會進行耗時操作,所以直接回傳COROUTINE_SUSPENDED
,讓原先執(zhí)行該掛起函數的Thread
先 return
并執(zhí)行其他東西,而 suspendFunction
則在另一條 Thread
上把耗時任務完成。
Continuation { // GlobalScope.Lanuch()
var label = 0
fun invokeSuspend(result:Any):Any{
when(label){
0->{
val functionResult = suspendFunction("text",this)
lable = 1
if(functionResult == COROUTINE_SUSPENDED){
return functionResult
}
}
1->{
throwOnFailure(result)
break
}
}
val text = result as String
print(text)
}
}
等待 suspendFunction
的耗時任務完成后,利用傳入的 Continuation
的 resumeWith
把結果傳入,這個動作會執(zhí)行到掛起函數的invokeSuspend(result)
,并傳入結果,該動作就能讓掛起函數得到suspendFunction(String)
的結果。
PS:上面那段代碼實際上是偽代碼,實際業(yè)務會比這復雜的多
所以事實上,掛起函數就是我把我的 callback
給你,等你結束后再用我之前給你的 callback
回調給我,你把你的 callback
給我,等我結束后我用之前你給我的 callback
通知你。
掛起函數時如何自行切換線程的?
原則上,掛起函數在執(zhí)行時,就會決定好要用哪個 Dispatcher
,然后就會建立掛起點,一般情況下,會走到 startCoroutineCancellable
,然后執(zhí)行createCoroutineUnintercepted
,也就是上面提到的:resumeWith
和invokeSuspend
。
我們進入到startCoroutineCancellable
內部再看看:
internal fun <R, T> (suspend (R) -> T).startCoroutineCancellable(receiver: R, completion: Continuation<T>) =
runSafely(completion) {
createCoroutineUnintercepted(receiver, completion).intercepted().resumeCancellableWith(Result.success(Unit))
}
createCoroutineUnintercepted
最后會產出一個 Continuation
,而resumeCancellableWith
其實就是我們前面說到的初始化操作, 這行會去執(zhí)行狀態(tài)機 case 0
。
至于 intercepted()
,到底要攔截啥,其實就是把生成的 Continuation
攔截給指定的 ContinuationInterceptor
(這東西包裝在 CoroutineContext
里面,原則上在指定 Dispatcher
的時候就已經建立好了)
public fun intercepted(): Continuation<Any?> =
intercepted
?: (context[ContinuationInterceptor]?.interceptContinuation(this) ?: this)
.also { intercepted = it }
這里可以注意到 interceptContinuation(Continuation)
,可以用他追下去,發(fā)現他是 ContinuationInterceptor
的方法 ,再追下去可以發(fā)現CoroutineDispatcher
繼承了他:
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
DispatchedContinuation(this, continuation)
可以發(fā)現該動作產生了一個 DispatchedContinuation
,看看 DispatchedContinuation
,可以注意到剛才有提到的 resumeCancellableWith
inline fun resumeCancellableWith(result: Result<T>) {
val state = result.toState()
if (dispatcher.isDispatchNeeded(context)) {
_state = state
resumeMode = MODE_CANCELLABLE
dispatcher.dispatch(context, this)
} else {
executeUnconfined(state, MODE_CANCELLABLE) {
if (!resumeCancelled()) {
resumeUndispatchedWith(result)
}
}
}
}
原則上就是利用 dispatcher
來決定需不需要 dispatch
,沒有就直接執(zhí)行了resumeUndispatchedWith
@Suppress("NOTHING_TO_INLINE") // we need it inline to save us an entry on the stack
inline fun resumeUndispatchedWith(result: Result<T>) {
withCoroutineContext(context, countOrElement) {
continuation.resumeWith(result)
}
}
其實就是直接跑 continuation
的 resumeWith
。
那回頭看一下,其實就可以發(fā)現是 CoroutineDispatcher
決定要用什么 Thread
了。
public abstract class CoroutineDispatcher :
AbstractCoroutineContextElement(ContinuationInterceptor), ContinuationInterceptor {
public open fun isDispatchNeeded(context: CoroutineContext): Boolean = true
public abstract fun dispatch(context: CoroutineContext, block: Runnable)
@InternalCoroutinesApi
public open fun dispatchYield(context: CoroutineContext, block: Runnable) = dispatch(context, block)
public final override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> =
DispatchedContinuation(this, continuation)
@InternalCoroutinesApi
public override fun releaseInterceptedContinuation(continuation: Continuation<*>) {
(continuation as DispatchedContinuation<*>).reusableCancellableContinuation?.detachChild()
}
}
其實知道這個東西后,就可以向下去找它的 Child ,就能找到 HandlerDispatcher
了。
isDispatchNeeded
就是說是否需要切換線程
。
dispatch
則是切換線程
的操作。
可以看到這兩個方法在 HandlerDispatcher
的執(zhí)行:
override fun isDispatchNeeded(context: CoroutineContext): Boolean {
return !invokeImmediately || Looper.myLooper() != handler.looper
}
override fun dispatch(context: CoroutineContext, block: Runnable) {
handler.post(block)
}
可以看到CoroutineContext
根本沒有用到。
為什么呢?其實原因主要是: 掛起函數是
設計給 Kotlin
用的,并不是專門設計給 Android
用的,所以 Android
要用的話,還是需要實現 CoroutineDispatcher
的部分,這實際上是兩個體系的東西。那 CoroutineDispatcher
的 dispatch
有提供 CoroutineContext
,但不見的 Android
這邊會用到,所以就有這個情況了。
其他諸如 Dispatcher.Default
,他用到了 線程池(Executor)
,Dispatcher.IO
則是用到了一個叫 工作隊列(WorkQueue)
的東西。
所以每一個 Dispatcher
都有自己的一套實現,目前有提供四種 Dispatcher
。