協(xié)程使用說明:https://blog.csdn.net/tmacfrank/article/details/145084326
協(xié)程原理分析:
協(xié)程是用戶態(tài)的線程運行框架,并不是直接操作操作系統(tǒng)級別的線程對象,大部分情況下可以不涉及線程的銷毀、創(chuàng)建、回收、線程調(diào)度等操作系統(tǒng)級別的操作,所以協(xié)程比線程對操作系統(tǒng)的資源消耗更少,運行效率也更高
CoroutineScope(Dispatchers.Default).launch {
println("${Thread.currentThread().name}: test")
}
Thread.sleep(100)
輸出打印:
DefaultDispatcher-worker-1: test
上面為最簡單的協(xié)程使用例子,作用域(CoroutineScope)指定一種類型的派發(fā)器(Dispatchers),lambda表達式里面的任務就會在所指定的線程/線程池里面執(zhí)行,launch返回一個 Job 對象,Job 對象可以獲取協(xié)程任務的運行狀態(tài)、開始或者取消協(xié)程
一:協(xié)程上下文(CoroutineContext)
協(xié)程上下文的作用:資源獲取,配置管理等工作,是執(zhí)行環(huán)境的通用數(shù)據(jù)資源的統(tǒng)一管理者。是協(xié)程運行必須設置的參數(shù)。
以下對象本質(zhì)上都是協(xié)程上下文
- Job 協(xié)程句柄
- CoroutineName 協(xié)程名稱
- CoroutineDispatcher 協(xié)程調(diào)度器
- CoroutineExceptionHandler 協(xié)程異常處理器
launch函數(shù)里面實現(xiàn)了操作符重載,支持以上多種類型的上下文進行 + 操作,結(jié)合后的協(xié)程上下文變成 [xxxxxxx,xxxxxxxx,xxxxxxxx] 形式
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
}
第一個參數(shù)是協(xié)程上下文,可以指定 Dispatcher 類型
派發(fā)器的分類:
* Dispatchers.Default : 默認的派發(fā)器,會將協(xié)程派發(fā)到默認的線程池進行調(diào)度和執(zhí)行
* Dispatchers.Main : 主線程派發(fā)器,會將協(xié)程派發(fā)到主線程調(diào)度和執(zhí)行,Kotlin 默認沒有實現(xiàn)該調(diào)度器,需要實際的接入方提供具體的實現(xiàn),針對 Android 開發(fā)來說,需要額外添加 kotlinx-coroutines-android 的運行時依賴
* Dispatchers.Unconfined : 未限定協(xié)程執(zhí)行線程的派發(fā)器,該派發(fā)器會在調(diào)用線程中執(zhí)行該協(xié)程,當協(xié)程被掛起后,恢復時所在的線程由掛起函數(shù)決定(在掛起時,由掛起函數(shù)決定要將該協(xié)程派發(fā)到哪個線程執(zhí)行)
* Dispatchers.IO : IO 派發(fā)器,和 Default 派發(fā)器共用執(zhí)行線程池,默認為 64 和虛擬機中可用的處理器核心數(shù)中的最大值
第二個參數(shù)是調(diào)度策略,可以指定 CoroutineStart 類型
調(diào)度策略分類:
* CoroutineStart.DEFAULT : 默認的調(diào)度策略,會立刻將協(xié)程調(diào)度進入目標線程/線程池中的任務隊列,等待執(zhí)行,在實際執(zhí)行之前可以通過 Job.cancel 方法取消。
* CoroutineStart.LAZY: 懶調(diào)度策略,只有當協(xié)程需要被執(zhí)行的時候才會進行調(diào)度,調(diào)用方可以通過 Job.start 方法來觸發(fā)調(diào)度
* CoroutineStart. ATOMIC: 自動調(diào)度,和 DEFAULT 策略類似,會立刻調(diào)度協(xié)程到任務隊列中,區(qū)別在于該調(diào)度策略在協(xié)程實際執(zhí)行前無法取消
* CoroutineStart.UNDISPATCHED: 立刻在當前調(diào)用線程中執(zhí)行該協(xié)程,直到協(xié)程遇到第一個掛起函數(shù),恢復時根據(jù)協(xié)程上下文中的派發(fā)器來決定要將協(xié)程派發(fā)到哪個線程/線程池中繼續(xù)執(zhí)行。類似于 Unconfined 派發(fā)器的作用,區(qū)別在于該策略在協(xié)程掛起并恢復后的執(zhí)行線程由協(xié)程派發(fā)器決定。
二:調(diào)度原理
關(guān)鍵路徑
CoroutineStart.invoke -> CancellableKt.startCoroutineCancellable ->
生成 Continuation 對象 -> resumeCancellableWith -> 進入 dispatch 任務分發(fā)流程 ->
CoroutineScheduler. dispatch -> 創(chuàng)建任務、將任務添加到隊列、執(zhí)行任務 -> DispatchedTask.run ->
BaseContinuationImpl.resumeWith -> BaseContinuationImpl. invokeSuspend -> 執(zhí)行 掛起函數(shù),就是外部傳進來的 lambda 表達式
反編譯后,可以看到最終執(zhí)行的是 invokSuspend 函數(shù)
public final class CoroutinePrincipleKt {
public static final void main() {
Job job = BuildersKt.launch(CoroutineScopeKt.CoroutineScope((CoroutineContext)EmptyCoroutineContext.INSTANCE), (CoroutineContext)Dispatchers.getIO(), CoroutineStart.LAZY, (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object var1) {
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch (this.label) {
case 0:
ResultKt.throwOnFailure(var1);
StringBuilder var10000 = new StringBuilder();
Thread var10001 = Thread.currentThread();
Intrinsics.checkNotNullExpressionValue(var10001, "Thread.currentThread()");
String var2 = var10000.append(var10001.getName()).append(": test11").toString();
System.out.println(var2);
return Unit.INSTANCE;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}));
job.start();
Thread.sleep(100L);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
}
三:掛起、恢復
協(xié)程之所以高效的原因
CoroutineScope(Dispatchers.Default).launch {
println("${Thread.currentThread().id} : coroutine1 log 1")
delay(200)
print("${Thread.currentThread().id} : coroutine1 log 2")
}
Thread.sleep(80)
CoroutineScope(Dispatchers.Default).launch {
println("${Thread.currentThread().id} : coroutine2 log 1")
}
Thread.sleep(500)
運行上面的代碼后,會出現(xiàn)以下打印
10 : coroutine1 log 1
10 : coroutine2 log 1
10 : coroutine1 log 2
從打印的結(jié)果看到,協(xié)程做到了在不阻塞當前線程的情況下,實現(xiàn)了任務的掛起,另一個任務可以運行在相同的線程下,并且掛起結(jié)束后,掛起任務可以繼續(xù)在當前線程運行。
用同步的方式,實現(xiàn)了異步的效果,中間不涉及線程切換等操作系統(tǒng)級別的操作。
問題一:為什么協(xié)程會知道需要執(zhí)行另一個任務?
DefaultExecutor 內(nèi)部有單線程輪詢堆頂任務,通過 delay 時間比較,確保能及時運行任務隊列里面的任務。如果當前任務等待時間還沒到,就會運行其他等待時間更少或者無需等待的任務
問題二:協(xié)程怎么知道恢復后,到底需要從哪里繼續(xù)執(zhí)行剩余邏輯?
BuildersKt.launch$default(CoroutineScopeKt.CoroutineScope((CoroutineContext)Dispatchers.getDefault()), (CoroutineContext)null, (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var3 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
StringBuilder var10000;
Thread var10001;
String var2;
switch (this.label) {
case 0:
ResultKt.throwOnFailure($result);
var10000 = new StringBuilder();
var10001 = Thread.currentThread();
Intrinsics.checkNotNullExpressionValue(var10001, "Thread.currentThread()");
var2 = var10000.append(var10001.getId()).append(" : coroutine1 log 1").toString();
System.out.println(var2);
// 執(zhí)行完第一行 log 的打印,準備執(zhí)行 delay 函數(shù)掛起協(xié)程,這里把 label 設置為 1
this.label = 1;
if (DelayKt.delay(200L, this) == var3) {
return var3;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
var10000 = new StringBuilder();
var10001 = Thread.currentThread();
Intrinsics.checkNotNullExpressionValue(var10001, "Thread.currentThread()");
var2 = var10000.append(var10001.getId()).append(" : coroutine1 log 2").toString();
System.out.print(var2);
return Unit.INSTANCE;
}
@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}
public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 3, (Object)null);
執(zhí)行完掛起點后,會將 label 自增,實現(xiàn)恢復后執(zhí)行掛起點之后的邏輯
本質(zhì)上就是維護一個狀態(tài)機,通過 label 字段實現(xiàn)多個掛起點的邏輯還原
四、異常處理
// 創(chuàng)建一個協(xié)程異常處理上下文
val coroutineExceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->
val coroutineName = coroutineContext[CoroutineName]?.name
println("$coroutineName occurred exception: $throwable")
}
CoroutineScope(Dispatchers.Default).launch(coroutineExceptionHandler + CoroutineName("My coroutine")) {
println("${Thread.currentThread().name}: coroutine test 1")
throw RuntimeException("test exception")
}
Thread.sleep(1000)
打印結(jié)果:
DefaultDispatcher-worker-1: coroutine test 1
My coroutine occurred exception: java.lang.RuntimeException: test exception
如果外部不設置異常處理邏輯,協(xié)程內(nèi)部會有默認的異常兜底
五、協(xié)程作用域
CoroutineScope 的作用是方便批量管理協(xié)程,每一個作用域都有一個根 Job,作用域內(nèi)創(chuàng)建的協(xié)程會作為子 Job 添加到根 Job 上。
如果根 Job 被取消,則這個作用域下的 Job 對象,也會被取消。
作用域的分類:
* runBlocking:頂層函數(shù),它的第二個參數(shù)為接收者是CoroutineScope的函數(shù)字面量,可啟動協(xié)程。但是它會阻塞當前線程,主要用于測試。
* GlobalScope:全局協(xié)程作用域,通過GlobalScope創(chuàng)建的協(xié)程不會有父協(xié)程,可以把它稱為根協(xié)程。它啟動的協(xié)程的生命周期只受整個應用程序的生命周期的限制,且不能取消,在運行時會消耗一些內(nèi)存資源,這可能會導致內(nèi)存泄露,所以仍不適用于業(yè)務開發(fā)。
* coroutineScope:創(chuàng)建一個獨立的協(xié)程作用域,直到所有啟動的協(xié)程都完成后才結(jié)束自身。它是一個掛起函數(shù),需要運行在協(xié)程內(nèi)或掛起函數(shù)內(nèi)。當這個作用域中的任何一個子協(xié)程失敗時,這個作用域失敗,所有其他的子程序都被取消。為并行分解工作而設計的。
* supervisorScope:與coroutineScope類似,不同的是子協(xié)程的異常不會影響父協(xié)程,也不會影響其他子協(xié)程。(作用域本身的失敗(在block或取消中拋出異常)會導致作用域及其所有子協(xié)程失敗,但不會取消父協(xié)程。)
* MainScope:為UI組件創(chuàng)建主作用域。一個頂層函數(shù),上下文是SupervisorJob() + Dispatchers.Main,說明它是一個在主線程執(zhí)行的協(xié)程作用域,通過cancel對協(xié)程進行取消。推薦使用。
如果是Android運行環(huán)境,還有
* lifecycleScope:Lifecycle Ktx庫提供的具有生命周期感知的協(xié)程作用域,與Lifecycle綁定生命周期,生命周期被銷毀時,此作用域?qū)⒈蝗∠?。會與當前的UI組件綁定生命周期,界面銷毀時該協(xié)程作用域?qū)⒈蝗∠粫斐蓞f(xié)程泄漏,推薦使用。
* viewModelScope:與lifecycleScope類似,與ViewModel綁定生命周期,當ViewModel被清除時,這個作用域?qū)⒈蝗∠?。推薦使用。
以上兩個 Android 環(huán)境的作用域,會和界面、ViewModel 綁定,如果界面銷毀、ViewModel 銷毀,自身的作用域也會銷毀