協(xié)程 kotlin Coroutine
目錄:
1. Coroutine
的基本使用
1.1 小結(jié)
2. CoroutineScope
類 和 coroutineScope(xxx)
方法
- 2.1
CoroutineScope
使用的代碼示例
- 2.1.1 在Activity
中的使用
- 2.1.2 在ViewModel
中使用以及為什么要在ViewModel
中使用 - 2.2
ViewModel
自動銷毀CoroutineScope
的邏輯 - 2.3
withContext(xxx)
用作切換線程 - 2.4 小結(jié)
3. launch
-> 創(chuàng)建協(xié)程
- 3.1
launch()
的參數(shù)和返回結(jié)果說明 - 3.2 什么是
Job
- 3.3
CoroutineScope.async()
方法 - 3.4 小結(jié)
4. suspend
是什么逊脯,「掛起」作用是什么
- 4.1 「掛起函數(shù)」的使用和代碼運行分析
- 4.1.1 同一線程中代碼運行邏輯
- 4.1.2 在當(dāng)前線程中新建一個線程的代碼運行邏輯--未使用
suspend
- 4.1.3 使用了
suspend
標(biāo)注戒突, 代碼的運行邏輯
- 4.2 「非阻塞掛起」的含義
- 4.3 完整測試代碼以及執(zhí)行結(jié)果
- 4.4
suspend b()
運行時的線程切換 - 4.5 插入一個小點:調(diào)度器和線程池
- 4.6 「掛起函數(shù)」小結(jié)
5. 調(diào)度器 CoroutineDispatcher
- 5.1
CoroutineDispatcher
的種類
6. 說一說協(xié)程中常見的類
- 6.1
CoroutineContext
的繼承關(guān)系 - 6.2
Coroutine
的繼承關(guān)系
7. 總結(jié)
正文
想著把協(xié)程說清楚的目的,能不能說清楚,看看下面行不行拍皮。
coroutines
協(xié)程從 kotlin 1.3
開始發(fā)布正式版兆旬,不在是實驗階段了译秦。
修改地址 1.3 changeLog
github
地址: kotlinx.coroutines
目前協(xié)程已經(jīng)支持了多平臺两残,在 Android
中使用需要添加依賴:
先把協(xié)程中的部分類的繼承關(guān)系梳理一下,這里先簡單的用一張類繼承圖表示委刘,詳細(xì)的一些類的介紹丧没,會在下面的內(nèi)容逐漸涉及到。
1. Coroutine
的基本使用
官方示例代碼如下:
suspend fun main() = coroutineScope {
launch {
delay(1000)
println("Kotlin Coroutines World!")
}
println("Hello")
}
代碼運行結(jié)果如下:
Hello
Kotlin Coroutines World!
從運行結(jié)果來看锡移,launch{}
中的代碼應(yīng)該和外面的代碼不再同一個線程呕童,下面我們驗證一下。
我們把代碼稍微修改一下淆珊,再次運行一下:
suspend fun mainTest() {
coroutineScope {
println("11111 線程 是" + Thread.currentThread())
launch {
println("22222 線程 是" + Thread.currentThread())
delay(1000)
println("Kotlin Coroutines World!")
}
println("33333 線程 是" + Thread.currentThread())
}
}
這是代碼運行結(jié)果為:
11111 線程 是Thread[main,5,main]
33333 線程 是Thread[main,5,main]
22222 線程 是Thread[DefaultDispatcher-worker-1 @coroutine#1,5,main]
Kotlin Coroutines World!
我們發(fā)現(xiàn)夺饲,在 coroutineScope
中,默認(rèn)是和外部在同一個線程中的施符。而 launch {}
會切換到默認(rèn)的一個子線程中 DefaultDispatcher
, 而不會影響主線程 println("33333 線程 是"
的執(zhí)行往声。
這個代碼中,牽扯到三部分戳吝,
- 什么是
coroutineScope()
和CoroutineScope
- 什么是
launch
- 什么是
suspend
下面聊一下這三個部分是什么浩销,以及如何使用它們。
1.1 小結(jié)
上述內(nèi)容簡單的介紹了協(xié)程的基本使用以及代碼運行的線程關(guān)系听哭。
同時引入了三個部分:
CoroutineScope
launch
suspend
下面內(nèi)容會依次介紹慢洋。
2. CoroutineScope
類和 coroutineScope(xxx)
方法
CoroutineScope
是一個接口,它為協(xié)程定義了一個范圍「或者稱為 作用域
」陆盘,每一個協(xié)程創(chuàng)建者都是它的一個「擴(kuò)展方法」普筹。
上面的說法,意思是什么呢隘马?
- 1.首先協(xié)程在這個
Scope
內(nèi)運行太防,不能超過這個范圍。 -
2. 協(xié)程只有在
CoroutineScope
才能被創(chuàng)建
因為目前所有協(xié)程的創(chuàng)建方法酸员, 例如launch()
,async()
全部是CoroutineScope
的擴(kuò)展方法蜒车。
CoroutineScope
是一個接口, 源碼如下:
/**
*
*/
public interface CoroutineScope {
/**
* The context of this scope.
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
* Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
*
* By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
*/
public val coroutineContext: CoroutineContext
}
它里面包含一個成員變量 coroutineContext
沸呐, 是當(dāng)前 CoroutineScope
的 context
.
coroutineContext
可以翻譯成「協(xié)程上下文」醇王,但和Android
中的Context
有很大不同呢燥。
CoroutineContext
是一個協(xié)程各種元素的集合崭添。
后面再介紹CoroutineContext
coroutineScope{}
和 CoroutineScope
不同,coroutineScope{}
是一個方法, 它可以創(chuàng)建一個 CoroutineScope
并在里面運行一些代碼叛氨。
coroutineScope{}
這個會在什么時候結(jié)束呢呼渣?代碼注釋中寫著:
This function returns as soon as the given block and all its children coroutines are completed.
當(dāng)傳入的閉包和它里面所有的子協(xié)程都執(zhí)行完成時才會返回棘伴。因為它是一個 suspend
函數(shù),會在它里面所有的「內(nèi)容」都運行完屁置,才會結(jié)束焊夸。
2.1 CoroutineScope
使用的代碼示例
在源碼的注釋中,寫了它的使用示例蓝角。
2.2.1 在 Activity
中的使用
在 Activity
里阱穗,你可以這么使用:
class MyActivity : AppCompatActivity(), CoroutineScope by MainScope() {
override fun onDestroy() {
cancel() // cancel is extension on CoroutineScope
}
fun showSomeData() = launch {
// <- extension on current activity, launched in the main thread
// ... here we can use suspending functions or coroutine builders with other dispatchers
draw(data) // draw in the main thread
}
}
MyActivity
中實現(xiàn)了 CoroutineScope
接口,并且默認(rèn)是創(chuàng)建了一個 MainScope()
.
MainScope()
本質(zhì)上是Creates the main [CoroutineScope] for UI components.
是為主線程上創(chuàng)建了一個CoroutineScope
使鹅,即這個scope
里的協(xié)程運行在「主線程」(如果未特別指定其他線程的話)MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
Dispatchers
為「協(xié)程調(diào)度器」揪阶, 后面在介紹它。
上面為源碼中的示例患朱。
2.2.2 在 ViewModel
中使用以及為什么要在 ViewModel
中使用
一般情況下鲁僚,在 Android
我們更愿意把協(xié)程部分放入到 ViewModel
中使用,而不是在 Activity
或者 Fragment
中使用裁厅。
為什么呢? 在上面的示例代碼中冰沙,我們需要在 onDestroy()
中去手動調(diào)用一下 cancel()
-> MainScpe
會銷毀里面的協(xié)程。.
而在 ViewModel
中执虹,默認(rèn)有一個擴(kuò)展成員是 ViewModel.viewModelScope
, 且它會在 ViewModel
被銷毀時自動回收拓挥, 而 ViewModel
又是和 Activity
生命周期相關(guān)的,因此可以放心大膽使用袋励,會自動銷毀回收撞叽。
同時也是為了把耗時的操作和 UI
剝離,讓代碼更加的清晰, 代碼示例:
class FirstHomeViewModel : ViewModel() {
....
/**
* 獲取首頁 banner 信息
*/
fun getBannerData() {
viewModelScope.launch(IO) {
// 做一些網(wǎng)絡(luò)請求類似的操作
...
withContext(Main) {
...
}
}
}
}
在上述代碼中插龄,我們利用 viewModelScope.launch(IO)
在 IO
線程中創(chuàng)建了一個協(xié)程, 在該協(xié)程里面做一些耗時的操作愿棋,然后通過 withContext(Main)
切換到主線程,可以做一些刷新數(shù)據(jù)和 UI
的操作均牢。
可參考谷歌開源庫
plaid
: https://github.com/android/plaid
以及我的另外一篇文章:http://www.reibang.com/p/f5e16605d80c
2.2 ViewModel
自動銷毀 CoroutineScope
的邏輯
todo ViewModel
的自動銷毀
上面我們提到過糠雨,在 ViewModel
中是會自動釋放協(xié)程的,那么是如何實現(xiàn)的呢徘跪?
viewModelScope()
源碼如下:
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}
其中 setTagIfAbsent(xxx)
會把當(dāng)前 CloseableCoroutineScope
存放在 mBagOfTags
這個 hashMap
中甘邀。
當(dāng) ViewModel
被銷毀時會走 clear()
方法:
MainThread
final void clear() {
mCleared = true;
// Since clear() is final, this method is still called on mock objects
// and in those cases, mBagOfTags is null. It'll always be empty though
// because setTagIfAbsent and getTag are not final so we can skip
// clearing it
if (mBagOfTags != null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
}
onCleared();
}
這里,會把 mBagOfTags
這個 Map
中的所有 value
取出來垮庐,做一個 close
操作松邪,也就是在這里,對我們的 coroutinesScope
做了 close()
操作哨查,從而取消它以及取消它里面的所有協(xié)程逗抑。
2.3 withContext(xxx)
用作切換線程
當(dāng)然,我們使用協(xié)程,很多時候邮府,是需要一些耗時的操作在協(xié)程里面完成荧关,等到這個操作完成后,我們就需要再次切換到主線程執(zhí)行應(yīng)有的邏輯褂傀,那么在協(xié)程里面忍啤,給我們提供了 withContext(xxx)
方法,使我們可以很方便的來回切換到指定的線程仙辟。
有關(guān) withContext(xxx)
的定義:
/**
* Calls the specified suspending block with a given coroutine context, suspends until it completes, and returns the result.
*/
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T = suspendCoroutineUninterceptedOrReturn sc@ {
...
}
方法的含義為:在指定的 coroutineContext
中運行掛起的閉包同波,該方法會一只掛起直到它完成,并且返回閉包的執(zhí)行結(jié)果叠国。
它有兩個參數(shù)参萄,第一個用作指定在那個線程,第二個是要執(zhí)行的閉包邏輯煎饼。
源碼的注釋中還有一句話:This function uses dispatcher from the new context, shifting execution of the [block] into the different thread if a new dispatcher is specified, and back to the original dispatcher when it completes.
翻譯過來就是讹挎,在這個方法中,它會切換到新的調(diào)度器 「在這里可理解為在新的被指定的線程中」里執(zhí)行 block
的代碼吆玖,并且在它完成時筒溃,會自動回到原本的 dispatcher
中。
用更通俗的話就是: withContext()
在執(zhí)行時沾乘,首先會從 A
線程 切換到被你指定的 B
線程中怜奖,然后等到 withContext()
執(zhí)行結(jié)束會,它會自動再切換到 A
線程翅阵。
A->B: 切換線程到 B
B-->A: 執(zhí)行結(jié)束后婆殿,自定切回線程到 A
這也是 withContext()
的方便之處止状, 在 java
代碼中呻澜,沒有這種效果的類似實現(xiàn)据忘。
也因為 withContext()
可以自動把線程切回來的特性,從而消除了一些代碼的嵌套邏輯讹语,使得代碼更易懂钙皮, 再加上 suspend
掛起函數(shù)的特性,代碼瀏覽起來更加舒服顽决。
例如代碼:
fun getBannerData() {
viewModelScope.launch(IO) {
Log.i("zc_test", "11111 current thread is ${Thread.currentThread()}")
withContext(Main) {
Log.i("zc_test", "22222 current thread is ${Thread.currentThread()}")
}
Log.i("zc_test", "33333 current thread is ${Thread.currentThread()}")
}
}
運行結(jié)果為:
2019-12-19 15:40:51.786 14920-15029/com.chendroid.learning I/zc_test: 11111 current thread is Thread[DefaultDispatcher-worker-3,5,main]
2019-12-19 15:40:51.786 14920-14920/com.chendroid.learning I/zc_test: 22222 current thread is Thread[main,5,main]
2019-12-19 15:40:51.789 14920-15029/com.chendroid.learning I/zc_test: 33333 current thread is Thread[DefaultDispatcher-worker-3,5,main]
「11111」 和 「33333」 兩處位置所在的線程是一致的短条。
2.4 小結(jié)
上面我們寫了很多內(nèi)容,簡單的總結(jié)一下才菠,以防遺忘茸时。
CoroutineScope
是協(xié)程Coroutine
的作用域,只有在CoroutineScope
內(nèi)赋访,協(xié)程才可以被創(chuàng)建可都,且協(xié)程只能運行在這個范圍內(nèi)缓待。ViewModel
具有自動釋放CoroutineScope
的作用,是生命安全的汹粤。withContext(xxx)
可在協(xié)程內(nèi)切換線程命斧, 并且具有自動切回原線程的能力田晚。
3. 什么是 launch
-- 創(chuàng)建協(xié)程
上面很多地方嘱兼,都或多或少的使用到了 launch()
方法, 那么它到底是什么呢贤徒?有那些需要注意的地方呢芹壕?我們一起來看一下。
launch()
會在當(dāng)前的 coroutineScope
中新建一個協(xié)程接奈,它是開啟一個協(xié)程的一種方式踢涌。
正如在 「什么是 CoroutineScope
」 里面說的,launch()
是 CoroutineScope
的一個擴(kuò)展方法序宦。
官方源碼為:
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ù): context
, start
, block
睁壁, 返回結(jié)果為 Job
3.1 launch()
的參數(shù)和返回結(jié)果說明
context
為CoroutineContext
:
用于標(biāo)明當(dāng)前協(xié)程運行的CoroutineContext
,簡單來說就是當(dāng)前coroutine
運行在哪個調(diào)度器上, 在這里如果不指定的話互捌,默認(rèn)會繼承當(dāng)前viewModelScope
所在的主線程的主線程調(diào)度器潘明,即「Main = MainCoroutineDispatcher
」start: CoroutineStart
意思是coroutine
什么時候開始運行.
默認(rèn)為CoroutineStart.DEFAULT
, 意思是:立即根據(jù)它的CoroutineContext
執(zhí)行該協(xié)程。block
閉包秕噪, 會在一個suspend
掛起函數(shù)里面運行該閉包钳降。
在閉包中,是我們真正需要執(zhí)行的邏輯腌巾。返回結(jié)果為
Job
:
用于管理這個協(xié)程遂填,可采用job.cancel()
來取消這個協(xié)程的運行。
那么什么是 job
呢澈蝙?下面簡單聊一下 Job
3.2 什么是 Job
Job
中文意思是「工作」吓坚, 官方的定義為:它是一個可取消的,其生命周期最終為完成狀態(tài)的事物灯荧。
可以簡單的暫時把它理解為 coroutine
協(xié)程的一個代表凌唬,它可以獲取當(dāng)前協(xié)程的狀態(tài),也可以取消該協(xié)程的運行漏麦。
public interface Job : CoroutineContext.Element {
...
}
其實它也是 CoroutineContext
的一個子類客税,「Element
是 CoroutineContext
的一個子類」。
Job
有三種狀態(tài):
-
isActive
:true
表示該Job
已經(jīng)開始撕贞,且尚未結(jié)束和被取消掉更耻。 -
isCompleted
:true
表示該Job
已經(jīng)結(jié)束「包括失敗和被取消」 -
isCancelled
:true
表示該Job
被取消掉
在源碼中,有這么一些描述捏膨,可以看作一張圖秧均,我以一個表格的形式展示:
job
有一些狀態(tài)
State | isActive |
isCompleted |
isCancelled |
---|---|---|---|
New (optional initial state) | false |
false |
false |
Active (default initial state) | true |
false |
false |
Completing (transient state) | true |
false |
false |
Cancelling (transient state) | false |
false |
true |
Cancelled (final state) | false |
true |
true |
Completed (final state) | false |
true |
false |
生命周期流程圖:
從某個角度淺顯的理解食侮,Job
可代指一個協(xié)程 Coroutine
的各種狀態(tài)。
3.3 CoroutineScope.async()
方法
除了 launch()
之外目胡,在協(xié)程中還有一個和它類似的方法用于創(chuàng)建協(xié)程锯七,是 CoroutineScope.async()
.
async()
和 launch()
的最大不同是返回結(jié)果的不同,launch()
是返回一個 job
, 而 async()
返回的是 Deferred<T>
Deferred
的翻譯為:「推遲」誉己, 那它是什么呢眉尸?源碼如下:
public interface Deferred<out T> : Job {
....
}
額,其實它本身是一個 Job
的子類巨双,也就是說噪猾,Deferred
和 Job
的生命周期流程是一樣的,且也可控制 Coroutine
.
它是一個帶著結(jié)果 「result
」 Job
.
可通過調(diào)用 Deferred.await()
等待異步結(jié)果的返回筑累。
我們可以通過 async
實現(xiàn)兩個并發(fā)的網(wǎng)絡(luò)請求袱蜡,例如:
// todo
suspend fun testAsync() {
coroutineScope {
val time = measureTimeMillis {
val one = async { doSomethingsOne() }
val two = async { doSomethingsTwo() }
println("the result is ${one.await() + two.await()}")
}
println("完成時間為 time is $time ms")
}
}
private suspend fun doSomethingsOne(): Int {
// 假設(shè)做了些事情,耗時
delay(1000L)
return 17
}
private suspend fun doSomethingsTwo(): Int {
// 假設(shè)做了些事情慢宗,耗時
delay(1000L)
return 30
}
運行結(jié)果為下:
the result is 47
完成時間為 time is 1017 ms
這里時間是小于 2000 ms
的坪蚁,原因就是上面兩個協(xié)程是并發(fā)運行的。
當(dāng)然
await()
也是一個掛起函數(shù)
3.4 小結(jié)
上面內(nèi)容中镜沽,我們總結(jié)了
-
launch()
的作用—— 是用來新建一個協(xié)程敏晤。 -
launch()
中各個參數(shù)的函數(shù); -
launch()
的返回結(jié)果job
的意義淘邻,以及它能夠獲取到當(dāng)前協(xié)程的各種狀態(tài) - 創(chuàng)建協(xié)程的另外一種方式:
async()
的簡單說明
4. 什么是 suspend
我們已經(jīng)無數(shù)次在前面提到 suspend
掛起函數(shù)了茵典,那么「掛起函數(shù)」到底是代表著什么意思呢?「非阻塞掛起」又是什么意思呢宾舅?
4.1 「掛起函數(shù)」的使用和代碼運行分析
suspend
是 kotlin
中的一個關(guān)鍵字统阿,它本身的意思是「掛起」。
在 kotlin
中筹我,被它標(biāo)注的函數(shù)扶平,被稱為「掛起函數(shù)」。
suspend function should be called only from a coroutine or another suspend function
首先「掛起函數(shù)」只能在協(xié)程和另外一個掛起函數(shù)里面調(diào)用蔬蕊。
4.1.1 同一線程中代碼運行邏輯
以下面代碼為例结澄,假設(shè)三個方法都在同一個線程「主線程」運行:
a()
b()
c()
正常的同一線程的代碼邏輯,原本就是阻塞式的岸夯,:
-
a()
運行結(jié)束后麻献,b()
開始運行; -
b()
運行結(jié)束后猜扮,c()
開始運行勉吻;
a()->b(): a() 運行結(jié)束后 b() 執(zhí)行
b()->c(): b() 運行結(jié)束后 c() 執(zhí)行
4.1.2 在當(dāng)前線程中新建一個線程的代碼運行邏輯--未使用 suspend
如果 b()
中開啟了一個子線程去處理邏輯「異步了」,且不使用 suspend
標(biāo)注 b()
的代碼塊運行邏輯為:
-
a()
運行結(jié)束后旅赢,b()
開始運行齿桃; -
b()
函數(shù)中惑惶,部分在主線程中的代碼運行完后,它開啟的子線程代碼可能還沒運行短纵,c()
開始執(zhí)行
a()->b(): a() 運行結(jié)束后 b() 執(zhí)行
b()->c(): b() 中带污,在主線程運行結(jié)束后「子線程可能剛開始還沒結(jié)束」, c() 執(zhí)行
上述代碼香到,其實是說鱼冀,b()
的異步代碼可能會晚與 c()
去執(zhí)行,因為異步和兩個線程养渴,導(dǎo)致代碼不再阻塞雷绢。
4.1.3 使用了 suspend
標(biāo)注泛烙, 代碼的運行邏輯
-
a()
運行結(jié)束后理卑,b()
開始運行; -
b()
運行結(jié)束后「它的子線程也運行結(jié)束了」蔽氨,c()
才會開始運行藐唠;
a()->b(): a() 運行結(jié)束后 b() 執(zhí)行
b()->c(): b() 運行結(jié)束后 c() 執(zhí)行
可以看到使用了 suspend
標(biāo)注的函數(shù),會使得當(dāng)前代碼在該函數(shù)處處于等待它的完全運行結(jié)束鹉究。
而 suspend
掛起函數(shù)的完全運行結(jié)束是指:該函數(shù)中的所有代碼「可能包含一個新的子線程宇立、」均運行結(jié)束。
上述三中不同的代碼的運行自赔,其實是想告訴大家 suspend
這個關(guān)鍵字的作用是:
把原本異步的代碼妈嘹,再次變得同步。
當(dāng)天如果只是簡單的同步绍妨,那么肯定會有很多問題润脸,
例如主線程等待子線程運行結(jié)束的問題,這是很不科學(xué)的他去,與我們把耗時操作放入子線程運行的初衷不符毙驯。
當(dāng)然,協(xié)程當(dāng)然不存在這種問題灾测。它是如何解決的呢爆价?
下面說一說協(xié)程的 「非阻塞掛起」
4.2 「非阻塞掛起」
我們還以第三種代碼情況說明, 不過這次加入了更多的代碼 test2()
方法。
假設(shè)完整代碼為:
b()
為 suspend
標(biāo)注的掛起函數(shù), 其他為正常函數(shù)
以下為簡化代碼
fun mainTest() {
...
test()
test2() // 假設(shè) test2() 運行在主線程
...
}
fun test() {
a()
b()
c()
}
fun test2() {
...
}
代碼實際的執(zhí)行運行邏輯為:
-
mainTest()
中先執(zhí)行到test()
方法媳搪,先運行a()
-
a()
運行結(jié)束后铭段,「掛起函數(shù)」b()
開始運行; - 「掛起函數(shù)」
b()
的主線程代碼運行結(jié)束后秦爆,c()
并不會運行序愚,而是test2()
開始運行, - 等到「掛起函數(shù)」
b()
中開啟的子線程也運行結(jié)束后鲜结,c()
才會開始運行展运;
圖示為:
mainTest()->test(): 先執(zhí)行 test() 「主線程」
test()->a(): 順序執(zhí)行 a() 「主線程」
a()->b(): a() 結(jié)束后活逆,執(zhí)行掛起函數(shù) b() 「主線程」
b()-->test(): b() 中的主線程完成后,在切到子線程時拗胜,會標(biāo)志 test() 執(zhí)行結(jié)束 「主線程」
test()-->mainTest(): test() 執(zhí)行結(jié)束蔗候,會順序執(zhí)行 test2(), 「主線程」
b()->c(): 注釋 1
注:上圖中的注釋 1 為:當(dāng)掛起函數(shù)
b()
里面的子線程運行結(jié)束后,會被協(xié)程切換到主線程埂软,然后c()
開始運行锈遥。
從上面可以看到 suspend
的作用是在當(dāng)前代碼處 「1」
暫停運行,轉(zhuǎn)而去運行該線程本身其他地方的邏輯代碼勘畔,等到該掛起函數(shù)中的代碼運行結(jié)束后「它里面的和它里面的子線程子協(xié)程均運行結(jié)束后」所灸,才會在暫停處 「1」
繼續(xù)運行。
注: 上述代碼炫七,其實并不完全成立爬立,因為只能在「協(xié)程」或者「掛起函數(shù)」里面才可以調(diào)用「掛起函數(shù)」
b()
, 因此test()
并不成立万哪,這里用于說明代碼運行邏輯侠驯,故而簡化了代碼。后面會給出完整的代碼奕巍。
哪里可以提現(xiàn)出:「非阻塞式掛起」這個含義呢吟策?
就是因為在上面的代碼中,在 test()
中的 b()
處掛起時「本身為主線程」的止,并不會影響到主線程的執(zhí)行檩坚,因為 test2()
在主線程中為正常執(zhí)行,阻塞的只是該協(xié)程內(nèi)部的代碼诅福。
4.3 附上完全測試代碼以及執(zhí)行結(jié)果
代碼為:
fun test {
viewModelScope.launch {
println("viewModelScope.launch ${Thread.currentThread()}")
mainTest()
println("viewModelScope.launch 結(jié)束了 ${Thread.currentThread()}")
}
test2()
}
...
// mainTest() 方法
suspend fun mainTest() {
println("mainTest() start start start " + Thread.currentThread())
a()
b()
c()
println("mainTest() end end end" + Thread.currentThread())
}
// 普通函數(shù) test2()
fun test2() {
println("test2() doing doing doing " + Thread.currentThread())
}
//普通函數(shù) a()
fun a() {
println("a() doing doing doing " + Thread.currentThread())
}
//普通函數(shù) c()
fun c() {
println("c() doing doing doing " + Thread.currentThread())
}
// 掛起函數(shù) b()
suspend fun b() {
println("b() start start start" + Thread.currentThread())
coroutineScope {
println("11111 線程 是" + Thread.currentThread())
launch(IO) {
println("22222 線程 是" + Thread.currentThread())
delay(1000)
println("22222 線程結(jié)束" + Thread.currentThread())
}
println("33333 線程 是" + Thread.currentThread())
}
println("b() end end end" + Thread.currentThread())
}
運行結(jié)果為:
I/System.out: viewModelScope.launch Thread[main,5,main]
I/System.out: mainTest() start start start Thread[main,5,main]
a() doing doing doing Thread[main,5,main]
b() start start startThread[main,5,main]
I/System.out: 11111 線程 是Thread[main,5,main]
I/System.out: 33333 線程 是Thread[main,5,main]
I/System.out: 22222 線程 是Thread[DefaultDispatcher-worker-2,5,main] 「標(biāo)注 1」
I/System.out: test2() doing doing doing Thread[main,5,main] 「標(biāo)注 2」
I/System.out: 22222 線程結(jié)束Thread[DefaultDispatcher-worker-9,5,main]
I/System.out: b() end end endThread[main,5,main]
c() doing doing doing Thread[main,5,main]
mainTest() end end endThread[main,5,main]
viewModelScope.launch 結(jié)束了 Thread[main,5,main]
可以看到 test2()
的執(zhí)行是要早于 c()
方法的匾委。
從運行結(jié)果上可以看到是和我們的分析一致的。
4.4 suspend b()
運行時的線程切換
從運行結(jié)果的 log
上, 我們還可以看到當(dāng)前代碼執(zhí)行的線程信息权谁。
我們發(fā)現(xiàn) suspend b()
的運行中剩檀,
-
b() start ...
在主線程main
中 - 通過
b()
中的launch(IO)
我們切換到了IO
線程DefaultDispatcher-worker
中 - 但是
b()
中的子線程運行結(jié)束后,我們發(fā)現(xiàn)b() end
再次回答了主線程main
中
在上面的操作中旺芽,第三步中沪猴,我們并沒有顯示的調(diào)用切回主現(xiàn)場的代碼,我們卻回到了主線程采章。
由此說明:suspend
掛起函數(shù)在運行結(jié)束時會再次切換到原來的線程运嗜,真正的切換是有協(xié)程幫我們做的
值得一提的是,我們在上面說到
withContext()
也具有自動切換原線程的功能悯舟。
因為……
withContext()
本身就是一個「掛起函數(shù)」担租。
協(xié)程是怎么切換到原線程的呢?一家之言抵怎,我害怕說不清楚……慫
4.5 這里插入一個小的點奋救。
根據(jù)上面岭参,我們知道 suspend
標(biāo)注的掛起函數(shù),協(xié)程會自動幫我們切換到原線程尝艘。
看兩行 log
信息
...
I/System.out: 22222 線程 是Thread[DefaultDispatcher-worker-2,5,main]
...
I/System.out: 22222 線程結(jié)束Thread[DefaultDispatcher-worker-9,5,main]
-
首先
Thread[DefaultDispatcher-worker-2,5,main]
這三項分別是什么大部分人應(yīng)該都知道演侯,這是源碼
Thread.toString()
方法中的返回值.
第一個參數(shù)DefaultDispatcher-worker-2
代表的是當(dāng)前線程的名字getName()
.
第二個參數(shù)5
代表的是當(dāng)前線程的優(yōu)先級getPriority()
默認(rèn)是5
.
第三個參數(shù)main
代表的是當(dāng)前線程屬于哪個線程組。
-
為什么先后兩次線程會不一致背亥?
在下面的
5
部分CoroutineDispatcher
我們會有介紹秒际,IO
調(diào)度器,它里面對應(yīng)的是一個線程池狡汉。所以先后兩次線程名字不一樣娄徊。
但它們屬于同一線程池
**也屬于同一個調(diào)度器DefaultDispatcher
**
帶來了一個問題,為什么在一個協(xié)程中盾戴,先后兩次線程的名字不同了呢寄锐?
肯定是在哪里切換了線程,才會導(dǎo)致線程的名稱不同捻脖。
看代碼中锐峭,我們知道:
22222 線程 是
和22222 線程結(jié)束
是在同一個launch(IO){}
協(xié)程內(nèi)的;-
由于
delay()
是個suspend
掛起函數(shù)中鼠,根據(jù)上面的4.4
中的描述可婶,協(xié)程在「掛起函數(shù)」運行完成后,自動幫我們切回原線程援雇,但打印的結(jié)果表示其實在了另外一個線程中矛渴。所以更準(zhǔn)確得說法是:
協(xié)程在「掛起函數(shù)」運行結(jié)束后,會自動切回原來的調(diào)度器中惫搏。
然后調(diào)度器可能會根據(jù)它對應(yīng)的線程池具温,去選擇可用的線程繼續(xù)工作。
這里需要涉及到 CoroutineDispatcher
以及 ContinuationInterceptor
筐赔,這里不做過多介紹「內(nèi)容實在太多了……懶~」铣猩。
記住一點就行:所有協(xié)程啟動時「掛起后,再次運行也為啟動」茴丰,都會有一次 Continuation.resumeWith()
的操作达皿,這時調(diào)度器會重新調(diào)度一次,協(xié)程的運行可能會從線程池中的 A
線程切換到 B
這個線程上贿肩。
這也是上述 log
信息出現(xiàn)的線程名字不同的原因峦椰。
Continuation
的源碼如下:
/**
* Interface representing a continuation after a suspension point that returns a value of type `T`.
*/
SinceKotlin("1.3")
public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/
public val context: CoroutineContext
/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/
public fun resumeWith(result: Result<T>)
}
在有一個 suspend
掛起點后,它會代表著一個協(xié)程汰规,協(xié)程會存在 T
中汤功,通過 resumeWith(result: Result<T>)
會重新得到這個協(xié)程實例。
4.6 suspend
小結(jié)
上面溜哮,我們使用了大量的代碼和邏輯圖滔金,用于表示 suspend
在實際運行中起到的作用色解。
suspend
會使得當(dāng)前代碼的運行在該函數(shù)處「掛起「協(xié)程內(nèi)掛起」」。suspend
的掛起餐茵,并不會影響主線程的代碼執(zhí)行冒签,掛起的范圍也是我們上面提到的CoroutineScope
這個范圍內(nèi)。suspend
掛起函數(shù)具有在該函數(shù)運行結(jié)束后钟病,再次切回原線程的能力萧恕。當(dāng)然,這是協(xié)程內(nèi)部幫我們完成的肠阱。更準(zhǔn)確的說法是:協(xié)程會在掛起函數(shù)運行結(jié)束后票唆,自動切回原調(diào)度器的能力。
那么「調(diào)度器」 是指什么呢屹徘?下面簡單說一下走趋。
5. CoroutineDispatcher
協(xié)程中的調(diào)度器
首先它繼承于 AbstractCoroutineContextElement
, 并實現(xiàn)了 ContinuationInterceptor
接口。
它是 CoroutineContext
的一個子類噪伊。
上面的代碼分析中簿煌,我們使用的 launch()
, async()
, 有時我們傳遞了一個參數(shù)「Main
, IO
」,其實就是 CoroutineDispatcher
鉴吹。
在上面中姨伟,我們已經(jīng)見到了 Main
IO
兩個調(diào)度器。
ContinuationInterceptor
是協(xié)程攔截器豆励, 在這里暫時不討論它夺荒。
5.1 CoroutineDispatcher
的種類
CoroutineDispatcher
的種類,都在 Dispatchers
類里面良蒸,在 Android
中有一下四類:
-
Default: CoroutineDispatcher = createDefaultDispatcher()
默認(rèn)的調(diào)度器技扼, 在
Android
中對應(yīng)的為「線程池」。
在新建的協(xié)程中嫩痰,如果沒有指定dispatcher
和ContinuationInterceptor
則默認(rèn)會使用該dispatcher
剿吻。
線程池中會有多個線程。 -
Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher
在主線程「
UI
線程」中的調(diào)度器串纺。
只在主線程中, 單個線程丽旅。 Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
-
IO: CoroutineDispatcher = DefaultScheduler.IO
在
IO
線程的調(diào)度器,里面的執(zhí)行邏輯會運行在IO
線程, 一般用于耗時的操作造垛。
對應(yīng)的是「線程池」魔招,會有多個線程在里面。IO
和Default
共享了線程五辽。
6. 說一說協(xié)程里面常見的類
在文章的開頭办斑,有一張圖,里面有一些在協(xié)程中涉及到的類,現(xiàn)在再來看一下乡翅。
是不是比剛在文章的開頭看上去要親和很多鳞疲?
如果是,那么恭喜你蠕蚜,說明大部分內(nèi)容你都看到了尚洽,并且記在了心里,這么長且枯燥的內(nèi)容靶累,很看到這里都很不容易腺毫。贊的贊的
6.1 CoroutineContext
CoroutineContext
和我們經(jīng)常在代碼中使用到的 Context
差別是很大的,它們兩沒有任何關(guān)系挣柬。
CoroutineContext
是各種不同元素的集合潮酒。
源碼如下:
/**
* Persistent context for the coroutine. It is an indexed set of [Element] instances.
* An indexed set is a mix between a set and a map.
* Every element in this set has a unique [Key].
*/
SinceKotlin("1.3")
public interface CoroutineContext {
...
public operator fun <E : Element> get(key: Key<E>): E?
...
/**
* Key for the elements of [CoroutineContext]. [E] is a type of element with this key.
*/
public interface Key<E : Element>
/**
* An element of the [CoroutineContext]. An element of the coroutine context is a singleton context by itself.
*/
public interface Element : CoroutineContext {
...
}
}
它的繼承關(guān)系是怎樣的呢?
什么是 Element
? 什么是 Key
?
Element
是一個接口邪蛔,實現(xiàn)了CoroutineContext
,
代表著:CoroutineContext
的一個元素急黎,且為一個單例侧到。而
Key
是以Element
作為 key 的接口。
從 CoroutineContext
需要根據(jù) Key
獲取到它對應(yīng)的 Element
例如:
// 獲取當(dāng)前協(xié)程的 job
val job = coroutineContext[Job]
val continuationInterceptor = coroutineContext[ContinuationInterceptor]
如果你翻一翻源碼就會發(fā)現(xiàn)故源,在 Job
和 ContinuationInterceptor
中,必定會實現(xiàn) CoroutineContext.Element
接口心软,并且具有一個「伴生對象」 companion object Key : CoroutineContext.Key<XXX>
著蛙。
Job
是 CoroutineContext
中最為重要的元素耳贬,代表著協(xié)程的運行狀態(tài)等信息
6.2 Coroutine
和 Continuation
Coroutine
就是我們說的「協(xié)程」踏堡, CoroutineScope.launch()
是會創(chuàng)建一個 Coroutine
的實例。
Continuation
是延續(xù)的意思咒劲,當(dāng)一個協(xié)程被創(chuàng)建時顷蟆,就會有一個 Continuation
對應(yīng)著該協(xié)程,它也可代表著協(xié)程的狀態(tài)腐魂。
用下面的圖表示協(xié)程的繼承關(guān)系:
我們可以發(fā)現(xiàn) Coroutine
繼承和實現(xiàn)了大量的接口帐偎,有 Job
,Continuation
, CoroutineScope
目前創(chuàng)建的協(xié)程,如果不特別指定蛔屹,都是 StandaloneCoroutine
的實例削樊,會立馬執(zhí)行。
當(dāng)掛起后,需要重新執(zhí)行協(xié)程時漫贞,會調(diào)用 Continuation.resume()
再次得到該協(xié)程實例甸箱,然后開始調(diào)度運行。
7. 總結(jié)
一定要先說一句迅脐,一家之言芍殖,很多理解可能并不準(zhǔn)確,有錯誤還請指正谴蔑。
協(xié)程庫里面的元素太多了豌骏,上面我只是從使用的 API
接口入口,逐步介紹了涉及到的一些知識隐锭。
但協(xié)程里面的實現(xiàn)原理肯适,調(diào)度器,切換原調(diào)度器的操作等原理成榜,都未進(jìn)行深入說明。
協(xié)程內(nèi)容太多了刘绣,想到這里纬凤,已經(jīng)比我剛開始想的要多很多很多停士。
目前寫到的內(nèi)容恋技,也只是淺嘗輒止蜻底。
但我真心希望薄辅,這篇花費了大量時間去寫的文章站楚,能解決一些對協(xié)程的困惑,能對看到這篇文章的人起到幫助。
希望能盡快用起來協(xié)程舅踪,真正使用起來抽碌,就能明顯感受到它給代碼帶來的精簡和便利货徙。
參考文檔:
朱凱-協(xié)程
medium - easy coroutines
http://talentprince.github.io/2019/02/12/Deep-explore-kotlin-coroutines/
Kotlin1.3 協(xié)程Api詳解:CoroutineScope, CoroutineContext
破解 Kotlin 協(xié)程(3) - 協(xié)程調(diào)度篇
2019.12.26 by chendroid
這本是之前寫的文章了蠢棱,無奈元旦之前未能發(fā)出泻仙,趕在 2020 的開始玉转,發(fā)出來究抓。
祝 2020 年刺下,每個人都能付出得到收獲怠李。
所有的愿望都將實現(xiàn),如果你有勇氣追求它