網站首頁 編程語言 正文
Kotlin協程是什么
本文只是自己經過研究后,對 Kotlin 協程的理解概括,如有偏差,還請斧正。
簡要概括:
協程是 Kotlin 提供的一套線程 API 框架,可以很方便的做線程切換。 而且在不用關心線程調度的情況下,能輕松的做并發編程。也可以說協程就是一種并發設計模式。
下面是使用傳統線程和協程執行任務:
Thread{ //執行耗時任務 }.start() val executors = Executors.newCachedThreadPool() executors.execute { //執行耗時任務 } GlobalScope.launch(Dispatchers.IO) { //執行耗時任務 }
在實際應用開發中,通常是在主線中去啟動子線程執行耗時任務,等耗時任務執行完成,再將結果給主線程,然后刷新UI:
Thread{ //執行耗時任務 runOnMainThread { //獲取耗時任務結果,刷新UI } }.start() val executors = Executors.newCachedThreadPool() executors.execute { //執行耗時任務 runOnMainThread { //獲取耗時任務結果,刷新UI } } Observable.unsafeCreate<Unit> { //執行耗時任務 }.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe { //獲取耗時任務結果,刷新UI } GlobalScope.launch(Dispatchers.Main) { val result = withContext(Dispatchers.IO){ //執行耗時任務 } //直接拿到耗時任務結果,刷新UI refreshUI(result) }
從上面可以看到,使用Java 的 Thread
和 Executors
都需要手動去處理線程切換,這樣的代碼不僅不優雅,而且有一個重要問題,那就是要去處理與生命周期相關的上下文判斷,這導致邏輯變復雜,而且容易出錯。
RxJava 是一套優雅的異步處理框架,代碼邏輯簡化,可讀性和可維護性都很高,很好的幫我們處理線程切換操作。這在 Java 語言環境開發下,是如虎添翼,但是在 Kotlin 語言環境中開發,如今的協程就比 RxJava 更方便,或者說更有優勢。
下面看一個 Kotlin 中使用協程的例子:
GlobalScope.launch(Dispatchers.Main) { Log.d("TestCoroutine", "launch start: ${Thread.currentThread()}") val numbersTo50Sum = withContext(Dispatchers.IO) { //在子線程中執行 1-50 的自然數和 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) { //在子線程中執行 51-100 的自然數和 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()}")
控制臺輸出結果:
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
是一個函數,用于創建協程并將其函數主體的執行分派給相應的調度程序。 -
Dispatchers.MAIN
指示此協程應在為 UI 操作預留的主線程上執行。 -
Dispatchers.IO
指示此協程應在為 I/O 操作預留的線程上執行。 -
withContext(Dispatchers.IO)
將協程的執行操作移至一個 I/O 線程。
從控制臺輸出結果中,可以看出在計算 1-50 和 51-100 的自然數和的時候,線程是從主線程(Thread[main,5,main]
)切換到了協程的線程(DefaultDispatcher-worker-1,5,main
),這里計算 1-50 和 51-100 都是同一個子線程。
在這里有一個重要的現象,代碼從邏輯上看起來是同步的,并且啟動協程執行任務的時候,沒有阻塞主線程繼續執行相關操作,而且在協程中的異步任務執行完成之后,又自動切回了主線程。這就是 Kotlin 協程給開發做并發編程帶來的好處。這也是有個概念的來源: Kotlin 協程同步非阻塞。
同步非阻塞”是真的“同步非阻塞” 嗎?下面探究一下其中的貓膩,通過 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 協程也是通過回調接口來實現異步操作的,這也解釋了 Kotlin 協程只是讓代碼邏輯是同步非阻塞,但是實際上并沒有,只是 Kotlin 編譯器為代碼做了很多事情,這也是說 Kotlin 協程其實就是一套線程 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 的自然數和是兩個并發操作 val result = numbersTo50Sum.await() + numbers50To100Sum.await() Log.d("TestCoroutine", "launch end:result=$result ${Thread.currentThread()}") } Log.d("TestCoroutine", "Hello World!,${Thread.currentThread()}")
控制臺輸出結果:
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
創建了一個協程,它讓計算 1-50 和 51-100 的自然數和是兩個并發操作。上面控制臺輸出結果可以看到計算 1-50 的自然數和是在線程 Thread[DefaultDispatcher-worker-2,5,main]
中,而計算 51-100 的自然數和是在另一個線程Thread[DefaultDispatcher-worker-1,5,main]
中。
從上面的例子,協程在異步操作,也就是線程切換上:主線程啟動子線程執行耗時操作,耗時操作執行完成將結果更新到主線程的過程中,代碼邏輯簡化,可讀性高。
suspend是什么
suspend 直譯就是:掛起
suspend 是 Kotlin 語言中一個 關鍵字,用于修飾方法,當修飾方法時,表示這個方法只能被 suspend 修飾的方法調用或者在協程中被調用。
下面看一下將上面代碼案例拆分成幾個 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() } }
控制臺輸出結果:
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 關鍵字標記方法時,其實是告訴 Kotlin 從協程內調用方法。所以這個“掛起”,并不是說方法或函數被掛起,也不是說線程被掛起。
假設一個非 suspend 修飾的方法調用 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()
經過 Kotlin 編譯器編譯后變成了private final Object calcNumbers50To100Sum(Continuation $completion)
, suspend
消失了,方法多了一個參數 Continuation $completion
,所以 suspend
修飾 Kotlin 的方法或函數,編譯器會對此方法做特殊處理。
另外,suspend
修飾的方法,也預示著這個方法是 耗時方法,告訴方法調用者要使用協程。當執行 suspend
方法,也預示著要切換線程,此時主線程依然可以繼續執行,而協程里面的代碼可能被掛起了。
下面再稍為修改 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 }
控制臺輸出結果:
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]
主線程不受協程線程的影響。
總結
Kotlin 協程是一套線程 API 框架,在 Kotlin 語言環境下使用它做并發編程比傳統 Thread, Executors 和 RxJava 更有優勢,代碼邏輯上“同步非阻塞“,而且簡潔,易閱讀和維護。
suspend
是 Kotlin 語言中一個關鍵字,用于修飾方法,當修飾方法時,該方法只能被 suspend
修飾的方法和協程調用。此時,也預示著該方法是一個耗時方法,告訴調用者需要在協程中使用。
參考文檔:
Android 上的 Kotlin 協程
Coroutines guide
下一篇,將研究 Kotlin Flow。
原文鏈接:https://blog.csdn.net/wangjiang_qianmo/article/details/128520879
相關推薦
- 2022-11-21 Android性能優化之JVMTI與內存分配_Android
- 2022-09-14 python?特殊屬性及方法詳細解析_python
- 2022-05-08 ASP.NET?MVC從控制器中獲取URL值_實用技巧
- 2022-09-18 在while中使用cin>>a?為條件及注意事項說明_C 語言
- 2022-09-25 Linux基礎組件之基于循環數組的無鎖隊列
- 2022-07-12 Linux下Redis6集群安裝部署指南
- 2022-10-19 Python?np.where()的詳解以及代碼應用_python
- 2022-08-27 asp.net中MVC的處理流程詳解_基礎應用
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支