前言
協(xié)程系列文章:
- 一個小故事講明白進(jìn)程、線程、Kotlin 協(xié)程到底啥關(guān)系?
- 少年,你可知 Kotlin 協(xié)程最初的樣子?
- 講真,Kotlin 協(xié)程的掛起/恢復(fù)沒那么神秘(故事篇)
- 講真,Kotlin 協(xié)程的掛起/恢復(fù)沒那么神秘(原理篇)
- Kotlin 協(xié)程調(diào)度切換線程是時候解開真相了
- Kotlin 協(xié)程之線程池探索之旅(與Java線程池PK)
- Kotlin 協(xié)程之取消與異常處理探索之旅(上)
- Kotlin 協(xié)程之取消與異常處理探索之旅(下)
- 來,跟我一起擼Kotlin runBlocking/launch/join/async/delay 原理&使用
之前一些列的文章重點(diǎn)在于分析協(xié)程本質(zhì)原理,了解了協(xié)程的內(nèi)核再來看其它衍生的知識就比較容易了。
接下來這邊文章著重分析協(xié)程框架提供的一些重要的函數(shù)原理,通過本篇文章,你將了解到:
- runBlocking 使用與原理
- launch 使用與原理
- join 使用與原理
- async/await 使用與原理
- delay 使用與原理
1. runBlocking 使用與原理
默認(rèn)分發(fā)器的runBlocking
使用
老規(guī)矩,先上Demo:
fun testBlock() {
println("before runBlocking thread:${Thread.currentThread()}")
//①
runBlocking {
println("I'm runBlocking start thread:${Thread.currentThread()}")
Thread.sleep(2000)
println("I'm runBlocking end")
}
//②
println("after runBlocking:${Thread.currentThread()}")
}
runBlocking 開啟了一個新的協(xié)程,它的特點(diǎn)是:
協(xié)程執(zhí)行結(jié)束后才會執(zhí)行runBlocking 后的代碼。
也就是① 執(zhí)行結(jié)束后 ② 才會執(zhí)行。
可以看出,協(xié)程運(yùn)行在當(dāng)前線程,因此若是在協(xié)程里執(zhí)行了耗時函數(shù),那么協(xié)程之后的代碼只能等待,基于這個特性,runBlocking 經(jīng)常用于一些測試的場景。
runBlocking 可以定義返回值,比如返回一個字符串:
fun testBlock2() {
var name = runBlocking {
"fish"
}
println("name $name")
}
原理
#Builders.kt
public fun <T> runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T {
//當(dāng)前線程
val currentThread = Thread.currentThread()
//先看有沒有攔截器
val contextInterceptor = context[ContinuationInterceptor]
val eventLoop: EventLoop?
val newContext: CoroutineContext
//----------①
if (contextInterceptor == null) {
//不特別指定的話沒有攔截器,使用loop構(gòu)建Context
eventLoop = ThreadLocalEventLoop.eventLoop
newContext = GlobalScope.newCoroutineContext(context + eventLoop)
} else {
eventLoop = (contextInterceptor as? EventLoop)?.takeIf { it.shouldBeProcessedFromContext() }
?: ThreadLocalEventLoop.currentOrNull()
newContext = GlobalScope.newCoroutineContext(context)
}
//BlockingCoroutine 顧名思義,阻塞的協(xié)程
val coroutine = BlockingCoroutine<T>(newContext, currentThread, eventLoop)
//開啟
coroutine.start(CoroutineStart.DEFAULT, coroutine, block)
//等待協(xié)程執(zhí)行完成----------②
return coroutine.joinBlocking()
}
重點(diǎn)看①②。
先說①,因?yàn)槲覀儧]有指定分發(fā)器,因此會使用loop,實(shí)際創(chuàng)建的是BlockingEventLoop,它繼承自EventLoopImplBase,最終繼承自CoroutineDispatcher(注意此處是個重點(diǎn))。
根據(jù)我們之前分析的協(xié)程知識可知,協(xié)程啟動后會構(gòu)造DispatchedContinuation,然后依靠dispatcher將runnable 分發(fā)執(zhí)行,而這個dispatcher 即是BlockingEventLoop。
#EventLoop.common.kt
//重寫dispatch函數(shù)
public final override fun dispatch(context: CoroutineContext, block: Runnable) = enqueue(block)
public fun enqueue(task: Runnable) {
//將task 加入隊(duì)列,task = DispatchedContinuation
if (enqueueImpl(task)) {
unpark()
} else {
DefaultExecutor.enqueue(task)
}
}
BlockingEventLoop 的父類EventLoopImplBase 里有個成員變量:_queue,它是個隊(duì)列,用來存儲提交的任務(wù)。
再看②:
協(xié)程任務(wù)已經(jīng)提交到隊(duì)列里,就看啥時候取出來執(zhí)行了。
#Builders.kt
fun joinBlocking(): T {
try {
try {
while (true) {
//當(dāng)前線程已經(jīng)中斷了,直接退出
if (Thread.interrupted()) throw InterruptedException().also { cancelCoroutine(it) }
//如果eventLoop!= null,則從隊(duì)列里取出task并執(zhí)行
val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE
//協(xié)程執(zhí)行結(jié)束,跳出循環(huán)
if (isCompleted) break
//掛起線程,parkNanos 指的是掛起時間
parkNanos(this, parkNanos)
//當(dāng)線程被喚醒后,繼續(xù)while循環(huán)
}
} finally { // paranoia
}
}
//返回結(jié)果
return state as T
}
#EventLoop.common.kt
override fun processNextEvent(): Long {
//延遲隊(duì)列
val delayed = _delayed.value
//延遲隊(duì)列處理,這里在分析delay時再解釋
//從隊(duì)列里取出task
val task = dequeue()
if (task != null) {
//執(zhí)行task
task.run()
return 0
}
return nextTime
}
上面代碼的任務(wù)有兩個:
- 嘗試從隊(duì)列里取出Task。
- 若是沒有則掛起線程。
結(jié)合①②兩點(diǎn),再來過一下場景:
- 先創(chuàng)建協(xié)程,包裝為DispatchedContinuation,作為task。
- 分發(fā)task,將task加入到隊(duì)列里。
- 從隊(duì)列里取出task執(zhí)行,實(shí)際執(zhí)行的即是協(xié)程體。
- 當(dāng)3執(zhí)行完畢后,runBlocking()函數(shù)也就退出了。
其中虛線箭頭表示執(zhí)行先后順序。
由此可見,runBlocking()函數(shù)需要等待協(xié)程執(zhí)行完畢后才退出。
指定分發(fā)器的runBlocking
上個Demo在使用runBlocking 時沒有指定其分發(fā)器,若是指定了又是怎么樣的流程呢?
fun testBlock3() {
println("before runBlocking thread:${Thread.currentThread()}")
//①
runBlocking(Dispatchers.IO) {
println("I'm runBlocking start thread:${Thread.currentThread()}")
Thread.sleep(2000)
println("I'm runBlocking end")
}
//②
println("after runBlocking:${Thread.currentThread()}")
}
指定在子線程里進(jìn)行分發(fā)。
此處與默認(rèn)分發(fā)器最大的差別在于:
默認(rèn)分發(fā)器加入隊(duì)列、取出隊(duì)列都是同一個線程,而指定分發(fā)器后task不會加入到隊(duì)列里,task的調(diào)度執(zhí)行完全由指定的分發(fā)器完成。
也就是說,coroutine.joinBlocking()后,當(dāng)前線程一定會被掛起。等到協(xié)程執(zhí)行完畢后再喚醒當(dāng)前被掛起的線程。
喚醒之處在于:
#Builders.kt
override fun afterCompletion(state: Any?) {
// wake up blocked thread
if (Thread.currentThread() != blockedThread)
//blockedThread 即為調(diào)用coroutine.joinBlocking()后阻塞的線程
//Thread.currentThread() 為線程池的線程
//喚醒線程
unpark(blockedThread)
}
紅色部分比紫色部分先執(zhí)行,因此紅色部分執(zhí)行的線程會阻塞,等待紫色部分執(zhí)行完畢后將它喚醒,最后runBlocking()函數(shù)執(zhí)行結(jié)束了。
不管是否指定分發(fā)器,runBlocking() 都會阻塞等待協(xié)程執(zhí)行完畢。
2. launch 使用與原理
想必大家剛接觸協(xié)程的時候使用最多的還是launch啟動協(xié)程吧。
看個Demo:
fun testLaunch() {
var job = GlobalScope.launch {
println("hello job1 start")//①
Thread.sleep(2000)
println("hello job1 end")//②
}
println("continue...")//③
}
非常簡單,啟動一個線程,打印結(jié)果如下:
③一定比①②先打印,同時也說明launch()函數(shù)并不阻塞當(dāng)前線程。
關(guān)于協(xié)程原理,在之前的文章都有深入分析,此處不再贅述,以圖示之:
3. join 使用與原理
雖然launch()函數(shù)不阻塞線程,但是我們就想要知道協(xié)程執(zhí)行完畢沒,進(jìn)而根據(jù)結(jié)果確定是否繼續(xù)往下執(zhí)行,這時候該Job.join()出場了。
先看該函數(shù)的定義:
#Job.kt
public suspend fun join()
是個suspend 修飾的函數(shù),suspend 是咱們的老朋友了,說明協(xié)程執(zhí)行到該函數(shù)會掛起(當(dāng)前線程不阻塞,另有他用)。
繼續(xù)看其實(shí)現(xiàn):
#JobSupport.kt
public final override suspend fun join() {
//快速判斷狀態(tài),不耗時
if (!joinInternal()) { // fast-path no wait
coroutineContext.ensureActive()
return // do not suspend
}
//掛起的地方
return joinSuspend() // slow-path wait
}
//suspendCancellableCoroutine 典型的掛起操作
//cont 是封裝后的協(xié)程
private suspend fun joinSuspend() = suspendCancellableCoroutine<Unit> { cont ->
//執(zhí)行完這就掛起
//disposeOnCancellation 是將cont 記錄在當(dāng)前協(xié)程的state里,構(gòu)造為node
cont.disposeOnCancellation(invokeOnCompletion(handler = ResumeOnCompletion(cont).asHandler))
}
其中suspendCancellableCoroutine 是掛起的核心所在,關(guān)于掛起的詳細(xì)分析請移步:講真,Kotlin 協(xié)程的掛起沒那么神秘(原理篇)
joinSuspend()函數(shù)有2個作用:
- 將當(dāng)前協(xié)程體存儲到Job的state里(作為node)。
- 將當(dāng)前協(xié)程掛起。
什么時候恢復(fù)呢?當(dāng)然是協(xié)程執(zhí)行完成后。
#JobSupport.kt
private class ResumeOnCompletion(
private val continuation: Continuation<Unit>
) : JobNode() {
//continuation 為協(xié)程的包裝體,它里面有我們真正的協(xié)程體
//之后重新進(jìn)行分發(fā)
override fun invoke(cause: Throwable?) = continuation.resume(Unit)
}
當(dāng)協(xié)程執(zhí)行完畢,會例行檢查當(dāng)前的state是否有掛著需要執(zhí)行的node,剛好我們在joinSuspend()里放了node,于是找到該node,進(jìn)而找到之前的協(xié)程體再次進(jìn)行分發(fā)。根據(jù)協(xié)程狀態(tài)機(jī)的知識可知,這是第二次執(zhí)行協(xié)程體,因此肯定會執(zhí)行job.join()之后的代碼,于是乎看起來的效果就是:
job.join() 等待協(xié)程執(zhí)行完畢后才會往下執(zhí)行。
語言比較蒼白,來個圖:
注:此處省略了協(xié)程掛起等相關(guān)知識,如果對此有疑惑請閱讀之前的文章。
4. async/await 使用與原理
launch 有2點(diǎn)不足之處:協(xié)程執(zhí)行沒有返回值。
這點(diǎn)我們從它的定義很容易獲悉:
然而,在有些場景我們需要返回值,此時輪到async/await 出場了。
fun testAsync() {
runBlocking {
//啟動協(xié)程
var job = GlobalScope.async {
println("job1 start")
Thread.sleep(10000)
//返回值
"fish"
}
//等待協(xié)程執(zhí)行結(jié)束,并返回協(xié)程結(jié)果
var result = job.await()
println("result:$result")
}
}
運(yùn)行結(jié)果:
接著來看實(shí)現(xiàn)原理。
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
val newContext = newCoroutineContext(context)
//構(gòu)造DeferredCoroutine
val coroutine = if (start.isLazy)
LazyDeferredCoroutine(newContext, block) else
DeferredCoroutine<T>(newContext, active = true)
//coroutine == DeferredCoroutine
coroutine.start(start, coroutine, block)
return coroutine
}
與launch 啟動方式不同的是,async 的協(xié)程定義了返回值,是個泛型。并且async里使用的是DeferredCoroutine,顧名思義:延遲給結(jié)果的協(xié)程。
后面的流程都是一樣的,不再細(xì)說。
再來看Job.await(),它與Job.join()類似:
- 先判斷是否需要掛起,若是協(xié)程已經(jīng)結(jié)束/被取消,當(dāng)然就無需等待直接返回。
- 先將當(dāng)前協(xié)程體包裝到state里作為node存放,然后掛起協(xié)程。
- 等待async里的協(xié)程執(zhí)行完畢,再重新調(diào)度執(zhí)行await()之后的代碼。
- 此時協(xié)程的值已經(jīng)返回。
這里需要重點(diǎn)關(guān)注一下返回值是怎么傳遞過來的。
將testAsync()反編譯:
public final Object invokeSuspend(@NotNull Object $result) {
//result 為協(xié)程執(zhí)行結(jié)果
Object var6 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
Object var10000;
switch(this.label) {
case 0:
//第一次執(zhí)行這
ResultKt.throwOnFailure($result);
Deferred job = BuildersKt.async$default((CoroutineScope) GlobalScope.INSTANCE, (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object var1) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure(var1);
String var2 = "job1 start";
boolean var3 = false;
System.out.println(var2);
Thread.sleep(10000L);
return "fish";
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
}), 3, (Object)null);
this.label = 1;
//掛起
var10000 = job.await(this);
if (var10000 == var6) {
return var6;
}
break;
case 1:
//第二次執(zhí)行這
ResultKt.throwOnFailure($result);
//result 就是demo里的"fish"
var10000 = $result;
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
String result = (String)var10000;
String var4 = "result:" + result;
boolean var5 = false;
System.out.println(var4);
return Unit.INSTANCE;
}
很明顯,外層的協(xié)程(runBlocking)體會執(zhí)行2次。
第1次:調(diào)用invokeSuspend(xx),此時參數(shù)xx=Unit,后遇到await 被掛起。
第2次:子協(xié)程執(zhí)行結(jié)束并返回結(jié)果"fish",恢復(fù)外部協(xié)程時再次調(diào)用invokeSuspend(xx),此時參數(shù)xx="fish",并將參數(shù)保存下來,因此result 就有了值。
值得注意的是:
async 方式啟動的協(xié)程,若是協(xié)程發(fā)生了異常,不會像launch 那樣直接拋出,而是需要等待調(diào)用await()時拋出。
5. delay 使用與原理
線程可以被阻塞,協(xié)程可以被掛起,掛起后的協(xié)程等待時機(jī)成熟可以被恢復(fù)。
fun testDelay() {
GlobalScope.launch {
println("before getName")
var name = getUserName()
println("after getName name:$name")
}
}
suspend fun getUserName():String {
return withContext(Dispatchers.IO) {
//模擬網(wǎng)絡(luò)獲取
Thread.sleep(2000)
"fish"
}
}
獲取用戶名字是在子線程獲取的,它是個掛起函數(shù),當(dāng)協(xié)程執(zhí)行到此時掛起,等待獲取名字之后再恢復(fù)運(yùn)行。
有時候我們僅僅只是想要協(xié)程掛起一段時間,并不需要去做其它操作,這個時候我們可以選擇使用delay(xx)函數(shù):
fun testDelay2() {
GlobalScope.launch {
println("before delay")
//協(xié)程掛起5s
delay(5000)
println("after delay")
}
}
再來看看其原理。
#Delay.kt
public suspend fun delay(timeMillis: Long) {
//沒必要延時
if (timeMillis <= 0) return // don't delay
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
//封裝協(xié)程為cont,便于之后恢復(fù)
if (timeMillis < Long.MAX_VALUE) {
//核心實(shí)現(xiàn)
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
}
主要看context.delay 實(shí)現(xiàn):
#DefaultExecutor.kt
internal actual val DefaultDelay: Delay = kotlinx.coroutines.DefaultExecutor
//單例
internal actual object DefaultExecutor : EventLoopImplBase(), Runnable {
const val THREAD_NAME = "kotlinx.coroutines.DefaultExecutor"
//...
private fun createThreadSync(): Thread {
return DefaultExecutor._thread ?: Thread(this, DefaultExecutor.THREAD_NAME).apply {
DefaultExecutor._thread = this
isDaemon = true
start()
}
}
//...
override fun run() {
//循環(huán)檢測隊(duì)列是否有內(nèi)容需要處理
//決定是否要掛起線程
}
//...
}
DefaultExecutor 是個單例,它里邊開啟了線程,并且檢測隊(duì)列里任務(wù)的情況來決定是否需要掛起線程等待。
先看隊(duì)列的出入隊(duì)情況。
放入隊(duì)列
我們注意到DefaultExecutor 繼承自EventLoopImplBase(),在最開始分析runBlocking()時有提到過它里面有成員變量_queue 存儲隊(duì)列元素,實(shí)際上它還有另一個成員變量_delayed:
#EventLoop.common.kt
internal abstract class EventLoopImplBase: EventLoopImplPlatform(), Delay {
//存放正常task
private val _queue = atomic<Any?>(null)
//存放延遲task
private val _delayed = atomic<EventLoopImplBase.DelayedTaskQueue?>(null)
}
private inner class DelayedResumeTask(
nanoTime: Long,
private val cont: CancellableContinuation<Unit>
) : EventLoopImplBase.DelayedTask(nanoTime) {
//協(xié)程恢復(fù)
override fun run() { with(cont) { resumeUndispatched(Unit) } }
override fun toString(): String = super.toString() + cont.toString()
}
delay.scheduleResumeAfterDelay 本質(zhì)是創(chuàng)建task:DelayedResumeTask,并將該task加入到延遲隊(duì)列_delayed里。
從隊(duì)列取出
DefaultExecutor 一開始就會調(diào)用processNextEvent()函數(shù)檢測隊(duì)列是否有數(shù)據(jù),如果沒有則將線程掛起一段時間(由processNextEvent()返回值確定)。
那么重點(diǎn)轉(zhuǎn)移到processNextEvent()上。
##EventLoop.common.kt
override fun processNextEvent(): Long {
if (processUnconfinedEvent()) return 0
val delayed = _delayed.value
if (delayed != null && !delayed.isEmpty) {
//調(diào)用delay 后會放入
//查看延遲隊(duì)列是否有任務(wù)
val now = nanoTime()
while (true) {
//一直取任務(wù),直到取不到(時間未到)
delayed.removeFirstIf {
//延遲任務(wù)時間是否已經(jīng)到了
if (it.timeToExecute(now)) {
//將延遲任務(wù)從延遲隊(duì)列取出,并加入到正常隊(duì)列里
enqueueImpl(it)
} else
false
} ?: break // quit loop when nothing more to remove or enqueueImpl returns false on "isComplete"
}
}
// 從正常隊(duì)列里取出
val task = dequeue()
if (task != null) {
//執(zhí)行
task.run()
return 0
}
//返回線程需要掛起的時間
return nextTime
}
而執(zhí)行任務(wù)最終就是執(zhí)行DelayedResumeTask.run()函數(shù),該函數(shù)里會對協(xié)程進(jìn)行恢復(fù)。
至此,delay 流程就比較清晰了:
- 構(gòu)造task 加入到延遲隊(duì)列里,此時協(xié)程掛起。
- 有個單獨(dú)的線程會檢測是否需要取出task并執(zhí)行,沒到時間的話就要掛起等待。
- 時間到了從延遲隊(duì)列里取出并放入正常的隊(duì)列,并從正常隊(duì)列里取出執(zhí)行。
- task 執(zhí)行的過程就是協(xié)程恢復(fù)的過程。
老規(guī)矩,上圖:
圖上虛線紫色框部分表明delay 執(zhí)行到此就結(jié)束了,協(xié)程掛起(不阻塞當(dāng)前線程),剩下的就交給單例的DefaultExecutor 調(diào)度,等待延遲的時間結(jié)束后通知協(xié)程恢復(fù)即可。
關(guān)于協(xié)程一些常用的函數(shù)分析到此就結(jié)束了,下篇開始我們一起探索協(xié)程通信(Channel/Flow 等)相關(guān)知識。
由于篇幅原因,省略了一些源碼的分析,若你對此有疑惑,可評論或私信小魚人。
本文基于Kotlin 1.5.3,文中完整Demo請點(diǎn)擊
您若喜歡,請點(diǎn)贊、關(guān)注、收藏,您的鼓勵是我前進(jìn)的動力
持續(xù)更新中,和我一起步步為營系統(tǒng)、深入學(xué)習(xí)Android/Kotlin
1、Android各種Context的前世今生
2、Android DecorView 必知必會
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分發(fā)全套服務(wù)
6、Android invalidate/postInvalidate/requestLayout 徹底厘清
7、Android Window 如何確定大小/onMeasure()多次執(zhí)行原因
8、Android事件驅(qū)動Handler-Message-Looper解析
9、Android 鍵盤一招搞定
10、Android 各種坐標(biāo)徹底明了
11、Android Activity/Window/View 的background
12、Android Activity創(chuàng)建到View的顯示過
13、Android IPC 系列
14、Android 存儲系列
15、Java 并發(fā)系列不再疑惑
16、Java 線程池系列
17、Android Jetpack 前置基礎(chǔ)系列
18、Android Jetpack 易懂易學(xué)系列
19、Kotlin 輕松入門系列
20、Kotlin 協(xié)程系列全面解讀