Kotlin關(guān)于協(xié)程是什么的探究
Kotlin協(xié)程是什么
本文只是自己經(jīng)過研究后,對 Kotlin 協(xié)程的理解概括,如有偏差,還請斧正。
簡要概括:
協(xié)程是 Kotlin 提供的一套線程 API 框架,可以很方便的做線程切換。 而且在不用關(guān)心線程調(diào)度的情況下,能輕松的做并發(fā)編程。也可以說協(xié)程就是一種并發(fā)設(shè)計模式。
下面是使用傳統(tǒng)線程和協(xié)程執(zhí)行任務:
Thread{
//執(zhí)行耗時任務
}.start()
val executors = Executors.newCachedThreadPool()
executors.execute {
//執(zhí)行耗時任務
}
GlobalScope.launch(Dispatchers.IO) {
//執(zhí)行耗時任務
}
在實際應用開發(fā)中,通常是在主線中去啟動子線程執(zhí)行耗時任務,等耗時任務執(zhí)行完成,再將結(jié)果給主線程,然后刷新UI:
Thread{
//執(zhí)行耗時任務
runOnMainThread {
//獲取耗時任務結(jié)果,刷新UI
}
}.start()
val executors = Executors.newCachedThreadPool()
executors.execute {
//執(zhí)行耗時任務
runOnMainThread {
//獲取耗時任務結(jié)果,刷新UI
}
}
Observable.unsafeCreate<Unit> {
//執(zhí)行耗時任務
}.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe {
//獲取耗時任務結(jié)果,刷新UI
}
GlobalScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO){
//執(zhí)行耗時任務
}
//直接拿到耗時任務結(jié)果,刷新UI
refreshUI(result)
}
從上面可以看到,使用Java 的 Thread 和 Executors 都需要手動去處理線程切換,這樣的代碼不僅不優(yōu)雅,而且有一個重要問題,那就是要去處理與生命周期相關(guān)的上下文判斷,這導致邏輯變復雜,而且容易出錯。
RxJava 是一套優(yōu)雅的異步處理框架,代碼邏輯簡化,可讀性和可維護性都很高,很好的幫我們處理線程切換操作。這在 Java 語言環(huán)境開發(fā)下,是如虎添翼,但是在 Kotlin 語言環(huán)境中開發(fā),如今的協(xié)程就比 RxJava 更方便,或者說更有優(yōu)勢。
下面看一個 Kotlin 中使用協(xié)程的例子:
GlobalScope.launch(Dispatchers.Main) {
Log.d("TestCoroutine", "launch start: ${Thread.currentThread()}")
val numbersTo50Sum = withContext(Dispatchers.IO) {
//在子線程中執(zhí)行 1-50 的自然數(shù)和
Log.d("TestCoroutine", "launch:numbersTo50Sum: ${Thread.currentThread()}")
delay(1000)
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo50 = naturalNumbers.takeWhile { it <= 50 }
numbersTo50.sum()
}
val numbers50To100Sum = withContext(Dispatchers.IO) {
//在子線程中執(zhí)行 51-100 的自然數(shù)和
Log.d("TestCoroutine", "launch:numbers50To100Sum: ${Thread.currentThread()}")
delay(1000)
val naturalNumbers = generateSequence(51) { it + 1 }
val numbers50To100 = naturalNumbers.takeWhile { it in 51..100 }
numbers50To100.sum()
}
val result = numbersTo50Sum + numbers50To100Sum
Log.d("TestCoroutine", "launch end:result=$result ${Thread.currentThread()}")
}
Log.d("TestCoroutine", "Hello World!,${Thread.currentThread()}")
控制臺輸出結(jié)果:
2023-01-02 16:05:45.846 10153-10153/com.wangjiang.example D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-02 16:05:48.058 10153-10153/com.wangjiang.example D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-02 16:05:48.059 10153-10322/com.wangjiang.example D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-02 16:05:49.114 10153-10322/com.wangjiang.example D/TestCoroutine: launch:numbers50To100Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-02 16:05:50.376 10153-10153/com.wangjiang.example D/TestCoroutine: launch end:result=5050 Thread[main,5,main]
在上面的代碼中:
launch是一個函數(shù),用于創(chuàng)建協(xié)程并將其函數(shù)主體的執(zhí)行分派給相應的調(diào)度程序。Dispatchers.MAIN指示此協(xié)程應在為 UI 操作預留的主線程上執(zhí)行。Dispatchers.IO指示此協(xié)程應在為 I/O 操作預留的線程上執(zhí)行。withContext(Dispatchers.IO)將協(xié)程的執(zhí)行操作移至一個 I/O 線程。
從控制臺輸出結(jié)果中,可以看出在計算 1-50 和 51-100 的自然數(shù)和的時候,線程是從主線程(Thread[main,5,main])切換到了協(xié)程的線程(DefaultDispatcher-worker-1,5,main),這里計算 1-50 和 51-100 都是同一個子線程。
在這里有一個重要的現(xiàn)象,代碼從邏輯上看起來是同步的,并且啟動協(xié)程執(zhí)行任務的時候,沒有阻塞主線程繼續(xù)執(zhí)行相關(guān)操作,而且在協(xié)程中的異步任務執(zhí)行完成之后,又自動切回了主線程。這就是 Kotlin 協(xié)程給開發(fā)做并發(fā)編程帶來的好處。這也是有個概念的來源: Kotlin 協(xié)程同步非阻塞。
同步非阻塞”是真的“同步非阻塞” 嗎?下面探究一下其中的貓膩,通過 Android Studio ,查看 .class 文件中的上面一段代碼:
BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)Dispatchers.getMain(), (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int I$0;
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var10000;
int numbersTo50Sum;
label17: {
Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
Function2 var10001;
CoroutineContext var6;
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
Log.d("TestCoroutine", "launch start: " + Thread.currentThread());
var6 = (CoroutineContext)Dispatchers.getIO();
var10001 = (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
Log.d("TestCoroutine", "launch:numbersTo50Sum: " + Thread.currentThread());
this.label = 1;
if (DelayKt.delay(1000L, this) == var4) {
return var4;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
Sequence naturalNumbers = SequencesKt.generateSequence(Boxing.boxInt(0), (Function1)null.INSTANCE);
Sequence numbersTo50 = SequencesKt.takeWhile(naturalNumbers, (Function1)null.INSTANCE);
return Boxing.boxInt(SequencesKt.sumOfInt(numbersTo50));
}
@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);
}
});
this.label = 1;
var10000 = BuildersKt.withContext(var6, var10001, this);
if (var10000 == var5) {
return var5;
}
break;
case 1:
ResultKt.throwOnFailure($result);
var10000 = $result;
break;
case 2:
numbersTo50Sum = this.I$0;
ResultKt.throwOnFailure($result);
var10000 = $result;
break label17;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
numbersTo50Sum = ((Number)var10000).intValue();
var6 = (CoroutineContext)Dispatchers.getIO();
var10001 = (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
Log.d("TestCoroutine", "launch:numbers50To100Sum: " + Thread.currentThread());
this.label = 1;
if (DelayKt.delay(1000L, this) == var4) {
return var4;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
Sequence naturalNumbers = SequencesKt.generateSequence(Boxing.boxInt(51), (Function1)null.INSTANCE);
Sequence numbers50To100 = SequencesKt.takeWhile(naturalNumbers, (Function1)null.INSTANCE);
return Boxing.boxInt(SequencesKt.sumOfInt(numbers50To100));
}
@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);
}
});
this.I$0 = numbersTo50Sum;
this.label = 2;
var10000 = BuildersKt.withContext(var6, var10001, this);
if (var10000 == var5) {
return var5;
}
}
int numbers50To100Sum = ((Number)var10000).intValue();
int result = numbersTo50Sum + numbers50To100Sum;
Log.d("TestCoroutine", "launch end:result=" + result + ' ' + Thread.currentThread());
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);
}
}), 2, (Object)null);
Log.d("TestCoroutine", "Hello World!," + Thread.currentThread());
雖然上面 .class 文件中的代碼比較復雜,但是從大體邏輯可以看出,Kotlin 協(xié)程也是通過回調(diào)接口來實現(xiàn)異步操作的,這也解釋了 Kotlin 協(xié)程只是讓代碼邏輯是同步非阻塞,但是實際上并沒有,只是 Kotlin 編譯器為代碼做了很多事情,這也是說 Kotlin 協(xié)程其實就是一套線程 API 框架的原因。
再看一個上面例子的變種:
GlobalScope.launch(Dispatchers.Main) {
Log.d("TestCoroutine", "launch start: ${Thread.currentThread()}")
val numbersTo50Sum = async {
withContext(Dispatchers.IO) {
Log.d("TestCoroutine", "launch:numbersTo50Sum: ${Thread.currentThread()}")
delay(2000)
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo50 = naturalNumbers.takeWhile { it <= 50 }
numbersTo50.sum()
}
}
val numbers50To100Sum = async {
withContext(Dispatchers.IO) {
Log.d("TestCoroutine", "launch:numbers50To100Sum: ${Thread.currentThread()}")
delay(500)
val naturalNumbers = generateSequence(51) { it + 1 }
val numbers50To100 = naturalNumbers.takeWhile { it in 51..100 }
numbers50To100.sum()
}
}
// 計算 1-50 和 51-100 的自然數(shù)和是兩個并發(fā)操作
val result = numbersTo50Sum.await() + numbers50To100Sum.await()
Log.d("TestCoroutine", "launch end:result=$result ${Thread.currentThread()}")
}
Log.d("TestCoroutine", "Hello World!,${Thread.currentThread()}")控制臺輸出結(jié)果:
2023-01-02 16:32:12.637 13303-13303/com.wangjiang.example D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-02 16:32:13.120 13303-13303/com.wangjiang.example D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-02 16:32:14.852 13303-13444/com.wangjiang.example D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-2,5,main]
2023-01-02 16:32:14.853 13303-13443/com.wangjiang.example D/TestCoroutine: launch:numbers50To100Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-02 16:32:17.462 13303-13303/com.wangjiang.example D/TestCoroutine: launch end:result=5050 Thread[main,5,main]
async 創(chuàng)建了一個協(xié)程,它讓計算 1-50 和 51-100 的自然數(shù)和是兩個并發(fā)操作。上面控制臺輸出結(jié)果可以看到計算 1-50 的自然數(shù)和是在線程 Thread[DefaultDispatcher-worker-2,5,main] 中,而計算 51-100 的自然數(shù)和是在另一個線程Thread[DefaultDispatcher-worker-1,5,main]中。
從上面的例子,協(xié)程在異步操作,也就是線程切換上:主線程啟動子線程執(zhí)行耗時操作,耗時操作執(zhí)行完成將結(jié)果更新到主線程的過程中,代碼邏輯簡化,可讀性高。
suspend是什么
suspend 直譯就是:掛起
suspend 是 Kotlin 語言中一個 關(guān)鍵字,用于修飾方法,當修飾方法時,表示這個方法只能被 suspend 修飾的方法調(diào)用或者在協(xié)程中被調(diào)用。
下面看一下將上面代碼案例拆分成幾個 suspend 方法:
fun getNumbersTo100Sum() {
GlobalScope.launch(Dispatchers.Main) {
Log.d("TestCoroutine", "launch start: ${Thread.currentThread()}")
val result = calcNumbers1To100Sum()
Log.d("TestCoroutine", "launch end:result=$result ${Thread.currentThread()}")
}
Log.d("TestCoroutine", "Hello World!,${Thread.currentThread()}")
}
private suspend fun calcNumbers1To100Sum(): Int {
return calcNumbersTo50Sum() + calcNumbers50To100Sum()
}
private suspend fun calcNumbersTo50Sum(): Int {
return withContext(Dispatchers.IO) {
Log.d("TestCoroutine", "launch:numbersTo50Sum: ${Thread.currentThread()}")
delay(1000)
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo50 = naturalNumbers.takeWhile { it <= 50 }
numbersTo50.sum()
}
}
private suspend fun calcNumbers50To100Sum(): Int {
return withContext(Dispatchers.IO) {
Log.d("TestCoroutine", "launch:numbers50To100Sum: ${Thread.currentThread()}")
delay(1000)
val naturalNumbers = generateSequence(51) { it + 1 }
val numbers50To100 = naturalNumbers.takeWhile { it in 51..100 }
numbers50To100.sum()
}
}
控制臺輸出結(jié)果:
2023-01-03 14:47:57.047 11349-11349/com.wangjiang.example D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-03 14:47:59.311 11349-11349/com.wangjiang.example D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-03 14:47:59.312 11349-11537/com.wangjiang.example D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-3,5,main]
2023-01-03 14:48:00.336 11349-11535/com.wangjiang.example D/TestCoroutine: launch:numbers50To100Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-03 14:48:01.339 11349-11349/com.wangjiang.example D/TestCoroutine: launch end:result=5050 Thread[main,5,main]
suspend 關(guān)鍵字標記方法時,其實是告訴 Kotlin 從協(xié)程內(nèi)調(diào)用方法。所以這個“掛起”,并不是說方法或函數(shù)被掛起,也不是說線程被掛起。
假設(shè)一個非 suspend 修飾的方法調(diào)用 suspend 修飾的方法會怎么樣呢?
private fun calcNumbersTo100Sum(): Int {
return calcNumbersTo50Sum() + calcNumbers50To100Sum()
}
此時,編譯器會提示:
Suspend function 'calcNumbersTo50Sum' should be called only from a coroutine or another suspend function
Suspend function 'calcNumbers50To100' should be called only from a coroutine or another suspend function
下面查看 .class 文件中的上面方法 calcNumbers50To100Sum 代碼:
private final Object calcNumbers50To100Sum(Continuation $completion) {
return BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
int label;
@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
Log.d("TestCoroutine", "launch:numbers50To100Sum: " + Thread.currentThread());
this.label = 1;
if (DelayKt.delay(1000L, this) == var4) {
return var4;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}
Sequence naturalNumbers = SequencesKt.generateSequence(Boxing.boxInt(51), (Function1)null.INSTANCE);
Sequence numbers50To100 = SequencesKt.takeWhile(naturalNumbers, (Function1)null.INSTANCE);
return Boxing.boxInt(SequencesKt.sumOfInt(numbers50To100));
}
@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);
}
}), $completion);
}可以看到 private suspend fun calcNumbers50To100Sum() 經(jīng)過 Kotlin 編譯器編譯后變成了private final Object calcNumbers50To100Sum(Continuation $completion), suspend 消失了,方法多了一個參數(shù) Continuation $completion,所以 suspend修飾 Kotlin 的方法或函數(shù),編譯器會對此方法做特殊處理。
另外,suspend 修飾的方法,也預示著這個方法是 耗時方法,告訴方法調(diào)用者要使用協(xié)程。當執(zhí)行 suspend 方法,也預示著要切換線程,此時主線程依然可以繼續(xù)執(zhí)行,而協(xié)程里面的代碼可能被掛起了。
下面再稍為修改 calcNumbers50To100Sum 方法:
private suspend fun calcNumbers50To100Sum(): Int {
Log.d("TestCoroutine", "launch:numbers50To100Sum:start: ${Thread.currentThread()}")
val sum= withContext(Dispatchers.Main) {
Log.d("TestCoroutine", "launch:numbers50To100Sum: ${Thread.currentThread()}")
delay(1000)
val naturalNumbers = generateSequence(51) { it + 1 }
val numbers50To100 = naturalNumbers.takeWhile { it in 51..100 }
numbers50To100.sum()
}
Log.d("TestCoroutine", "launch:numbers50To100Sum:end: ${Thread.currentThread()}")
return sum
}
控制臺輸出結(jié)果:
2023-01-03 15:28:04.349 15131-15131/com.bilibili.studio D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-03 15:28:04.803 15131-15131/com.bilibili.studio D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-03 15:28:04.804 15131-15266/com.bilibili.studio D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-3,5,main]
2023-01-03 15:28:06.695 15131-15131/com.bilibili.studio D/TestCoroutine: launch:numbers50To100Sum:start: Thread[main,5,main]
2023-01-03 15:28:06.696 15131-15131/com.bilibili.studio D/TestCoroutine: launch:numbers50To100Sum: Thread[main,5,main]
2023-01-03 15:28:07.700 15131-15131/com.bilibili.studio D/TestCoroutine: launch:numbers50To100Sum:end: Thread[main,5,main]
2023-01-03 15:28:07.700 15131-15131/com.bilibili.studio D/TestCoroutine: launch end:result=5050 Thread[main,5,main]
主線程不受協(xié)程線程的影響。
總結(jié)
Kotlin 協(xié)程是一套線程 API 框架,在 Kotlin 語言環(huán)境下使用它做并發(fā)編程比傳統(tǒng) Thread, Executors 和 RxJava 更有優(yōu)勢,代碼邏輯上“同步非阻塞“,而且簡潔,易閱讀和維護。
suspend 是 Kotlin 語言中一個關(guān)鍵字,用于修飾方法,當修飾方法時,該方法只能被 suspend 修飾的方法和協(xié)程調(diào)用。此時,也預示著該方法是一個耗時方法,告訴調(diào)用者需要在協(xié)程中使用。
參考文檔:
下一篇,將研究 Kotlin Flow。
到此這篇關(guān)于Kotlin關(guān)于協(xié)程是什么的探究的文章就介紹到這了,更多相關(guān)Kotlin協(xié)程內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android studio 實現(xiàn)隨機位置畫10個隨機大小的五角星的代碼
這篇文章主要介紹了Android studio 實現(xiàn)隨機位置畫10個隨機大小的五角星,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-05-05
flutter?material?widget組件之信息展示組件使用詳解
這篇文章主要為大家詳細介紹了flutter?material?widget組件之信息展示組件的使用,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-08-08
Android實現(xiàn)點擊切換視圖并跳轉(zhuǎn)傳值
這篇文章主要為大家詳細介紹了Android實現(xiàn)點擊切換視圖并跳轉(zhuǎn)傳值,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01
Android實現(xiàn)可輸入數(shù)據(jù)的彈出框
這篇文章主要為大家詳細介紹了Android實現(xiàn)可輸入數(shù)據(jù)的彈出框,文章提供了兩種方式,示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-01-01
Android ListView實現(xiàn)仿iPhone實現(xiàn)左滑刪除按鈕的簡單實例
下面小編就為大家?guī)硪黄狝ndroid ListView實現(xiàn)仿iPhone實現(xiàn)左滑刪除按鈕的簡單實例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-08-08
android studio3.0.1無法啟動Gradle守護進程的解決方法
這篇文章主要為大家詳細介紹了android studio3.0.1無法啟動Gradle守護進程的解決方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-08-08
Android控件之CheckBox、RadioButton用法實例分析
這篇文章主要介紹了Android控件之CheckBox、RadioButton用法,以實例形式較為詳細的分析了CheckBox和RadioButton實現(xiàn)復選按鈕及單選按鈕功能的相關(guān)技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-09-09

