協(xié)程 kotlin Coroutine
目錄:
1. Coroutine 的基本使用
1.1 小結
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 小結
3. launch -> 創(chuàng)建協(xié)程
- 3.1
launch()的參數(shù)和返回結果說明 - 3.2 什么是
Job - 3.3
CoroutineScope.async()方法 - 3.4 小結
4. suspend 是什么,「掛起」作用是什么
- 4.1 「掛起函數(shù)」的使用和代碼運行分析
- 4.1.1 同一線程中代碼運行邏輯
- 4.1.2 在當前線程中新建一個線程的代碼運行邏輯--未使用
suspend - 4.1.3 使用了
suspend標注, 代碼的運行邏輯
- 4.2 「非阻塞掛起」的含義
- 4.3 完整測試代碼以及執(zhí)行結果
- 4.4
suspend b()運行時的線程切換 - 4.5 插入一個小點:調(diào)度器和線程池
- 4.6 「掛起函數(shù)」小結
5. 調(diào)度器 CoroutineDispatcher
- 5.1
CoroutineDispatcher的種類
6. 說一說協(xié)程中常見的類
- 6.1
CoroutineContext的繼承關系 - 6.2
Coroutine的繼承關系
7. 總結
正文
想著把協(xié)程說清楚的目的,能不能說清楚,看看下面行不行。
coroutines 協(xié)程從 kotlin 1.3 開始發(fā)布正式版,不在是實驗階段了。
修改地址 1.3 changeLog
github 地址: kotlinx.coroutines
目前協(xié)程已經(jīng)支持了多平臺,在 Android 中使用需要添加依賴:

先把協(xié)程中的部分類的繼承關系梳理一下,這里先簡單的用一張類繼承圖表示,詳細的一些類的介紹,會在下面的內(nèi)容逐漸涉及到。

1. Coroutine 的基本使用
官方示例代碼如下:
suspend fun main() = coroutineScope {
launch {
delay(1000)
println("Kotlin Coroutines World!")
}
println("Hello")
}
代碼運行結果如下:
Hello
Kotlin Coroutines World!
從運行結果來看,launch{} 中的代碼應該和外面的代碼不再同一個線程,下面我們驗證一下。
我們把代碼稍微修改一下,再次運行一下:
suspend fun mainTest() {
coroutineScope {
println("11111 線程 是" + Thread.currentThread())
launch {
println("22222 線程 是" + Thread.currentThread())
delay(1000)
println("Kotlin Coroutines World!")
}
println("33333 線程 是" + Thread.currentThread())
}
}
這是代碼運行結果為:
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 中,默認是和外部在同一個線程中的。而 launch {}會切換到默認的一個子線程中 DefaultDispatcher, 而不會影響主線程 println("33333 線程 是"的執(zhí)行。
這個代碼中,牽扯到三部分,
- 什么是
coroutineScope()和CoroutineScope - 什么是
launch - 什么是
suspend
下面聊一下這三個部分是什么,以及如何使用它們。
1.1 小結
上述內(nèi)容簡單的介紹了協(xié)程的基本使用以及代碼運行的線程關系。
同時引入了三個部分:
CoroutineScopelaunchsuspend
下面內(nèi)容會依次介紹。
2. CoroutineScope 類和 coroutineScope(xxx) 方法
CoroutineScope 是一個接口,它為協(xié)程定義了一個范圍「或者稱為 作用域」,每一個協(xié)程創(chuàng)建者都是它的一個「擴展方法」。
上面的說法,意思是什么呢?
- 1.首先協(xié)程在這個
Scope內(nèi)運行,不能超過這個范圍。 -
2. 協(xié)程只有在
CoroutineScope才能被創(chuàng)建
因為目前所有協(xié)程的創(chuàng)建方法, 例如launch(),async()全部是CoroutineScope的擴展方法。
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, 是當前 CoroutineScope 的 context.
coroutineContext可以翻譯成「協(xié)程上下文」,但和Android中的Context有很大不同。
CoroutineContext是一個協(xié)程各種元素的集合。
后面再介紹CoroutineContext
coroutineScope{} 和 CoroutineScope 不同,coroutineScope{} 是一個方法, 它可以創(chuàng)建一個 CoroutineScope 并在里面運行一些代碼。
coroutineScope{} 這個會在什么時候結束呢?代碼注釋中寫著:
This function returns as soon as the given block and all its children coroutines are completed.
當傳入的閉包和它里面所有的子協(xié)程都執(zhí)行完成時才會返回。因為它是一個 suspend 函數(shù),會在它里面所有的「內(nèi)容」都運行完,才會結束。
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 接口,并且默認是創(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 中,默認有一個擴展成員是 ViewModel.viewModelScope, 且它會在 ViewModel 被銷毀時自動回收, 而 ViewModel 又是和 Activity 生命周期相關的,因此可以放心大膽使用,會自動銷毀回收。
同時也是為了把耗時的操作和 UI 剝離,讓代碼更加的清晰, 代碼示例:
class FirstHomeViewModel : ViewModel() {
....
/**
* 獲取首頁 banner 信息
*/
fun getBannerData() {
viewModelScope.launch(IO) {
// 做一些網(wǎng)絡請求類似的操作
...
withContext(Main) {
...
}
}
}
}
在上述代碼中,我們利用 viewModelScope.launch(IO) 在 IO 線程中創(chuàng)建了一個協(xié)程, 在該協(xié)程里面做一些耗時的操作,然后通過 withContext(Main) 切換到主線程,可以做一些刷新數(shù)據(jù)和 UI 的操作。
可參考谷歌開源庫
plaid: https://github.com/android/plaid
以及我的另外一篇文章:http://www.itdecent.cn/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) 會把當前 CloseableCoroutineScope 存放在 mBagOfTags 這個 hashMap 中。
當 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) 用作切換線程
當然,我們使用協(xié)程,很多時候,是需要一些耗時的操作在協(xié)程里面完成,等到這個操作完成后,我們就需要再次切換到主線程執(zhí)行應有的邏輯,那么在協(xié)程里面,給我們提供了 withContext(xxx) 方法,使我們可以很方便的來回切換到指定的線程。
有關 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í)行結果。
它有兩個參數(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í)行結束會,它會自動再切換到 A 線程。
A->B: 切換線程到 B
B-->A: 執(zhí)行結束后,自定切回線程到 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()}")
}
}
運行結果為:
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 小結
上面我們寫了很多內(nèi)容,簡單的總結一下,以防遺忘。
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() 會在當前的 coroutineScope 中新建一個協(xié)程,它是開啟一個協(xié)程的一種方式。
正如在 「什么是 CoroutineScope」 里面說的,launch() 是 CoroutineScope 的一個擴展方法。
官方源碼為:
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, 返回結果為 Job
3.1 launch() 的參數(shù)和返回結果說明
context為CoroutineContext:
用于標明當前協(xié)程運行的CoroutineContext,簡單來說就是當前coroutine運行在哪個調(diào)度器上, 在這里如果不指定的話,默認會繼承當前viewModelScope所在的主線程的主線程調(diào)度器,即「Main = MainCoroutineDispatcher」start: CoroutineStart意思是coroutine什么時候開始運行.
默認為CoroutineStart.DEFAULT, 意思是:立即根據(jù)它的CoroutineContext執(zhí)行該協(xié)程。block閉包, 會在一個suspend掛起函數(shù)里面運行該閉包。
在閉包中,是我們真正需要執(zhí)行的邏輯。返回結果為
Job:
用于管理這個協(xié)程,可采用job.cancel()來取消這個協(xié)程的運行。
那么什么是 job 呢?下面簡單聊一下 Job
3.2 什么是 Job
Job 中文意思是「工作」, 官方的定義為:它是一個可取消的,其生命周期最終為完成狀態(tài)的事物。
可以簡單的暫時把它理解為 coroutine 協(xié)程的一個代表,它可以獲取當前協(xié)程的狀態(tài),也可以取消該協(xié)程的運行。
public interface Job : CoroutineContext.Element {
...
}
其實它也是 CoroutineContext 的一個子類,「Element 是 CoroutineContext 的一個子類」。
Job 有三種狀態(tài):
-
isActive:true表示該Job已經(jīng)開始,且尚未結束和被取消掉。 -
isCompleted:true表示該Job已經(jīng)結束「包括失敗和被取消」 -
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() 的最大不同是返回結果的不同,launch()是返回一個 job, 而 async() 返回的是 Deferred<T>
Deferred 的翻譯為:「推遲」, 那它是什么呢?源碼如下:
public interface Deferred<out T> : Job {
....
}
額,其實它本身是一個 Job 的子類,也就是說,Deferred 和 Job 的生命周期流程是一樣的,且也可控制 Coroutine.
它是一個帶著結果 「result」 Job.
可通過調(diào)用 Deferred.await() 等待異步結果的返回。
我們可以通過 async 實現(xiàn)兩個并發(fā)的網(wǎng)絡請求,例如:
// 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 {
// 假設做了些事情,耗時
delay(1000L)
return 17
}
private suspend fun doSomethingsTwo(): Int {
// 假設做了些事情,耗時
delay(1000L)
return 30
}
運行結果為下:
the result is 47
完成時間為 time is 1017 ms
這里時間是小于 2000 ms 的,原因就是上面兩個協(xié)程是并發(fā)運行的。
當然
await()也是一個掛起函數(shù)
3.4 小結
上面內(nèi)容中,我們總結了
-
launch()的作用—— 是用來新建一個協(xié)程。 -
launch()中各個參數(shù)的函數(shù); -
launch()的返回結果job的意義,以及它能夠獲取到當前協(xié)程的各種狀態(tài) - 創(chuàng)建協(xié)程的另外一種方式:
async()的簡單說明
4. 什么是 suspend
我們已經(jīng)無數(shù)次在前面提到 suspend 掛起函數(shù)了,那么「掛起函數(shù)」到底是代表著什么意思呢?「非阻塞掛起」又是什么意思呢?
4.1 「掛起函數(shù)」的使用和代碼運行分析
suspend 是 kotlin 中的一個關鍵字,它本身的意思是「掛起」。
在 kotlin 中,被它標注的函數(shù),被稱為「掛起函數(shù)」。
suspend function should be called only from a coroutine or another suspend function
首先「掛起函數(shù)」只能在協(xié)程和另外一個掛起函數(shù)里面調(diào)用。
4.1.1 同一線程中代碼運行邏輯
以下面代碼為例,假設三個方法都在同一個線程「主線程」運行:
a()
b()
c()
正常的同一線程的代碼邏輯,原本就是阻塞式的,:
-
a()運行結束后,b()開始運行; -
b()運行結束后,c()開始運行;
a()->b(): a() 運行結束后 b() 執(zhí)行
b()->c(): b() 運行結束后 c() 執(zhí)行
4.1.2 在當前線程中新建一個線程的代碼運行邏輯--未使用 suspend
如果 b() 中開啟了一個子線程去處理邏輯「異步了」,且不使用 suspend 標注 b() 的代碼塊運行邏輯為:
-
a()運行結束后,b()開始運行; -
b()函數(shù)中,部分在主線程中的代碼運行完后,它開啟的子線程代碼可能還沒運行,c()開始執(zhí)行
a()->b(): a() 運行結束后 b() 執(zhí)行
b()->c(): b() 中,在主線程運行結束后「子線程可能剛開始還沒結束」, c() 執(zhí)行
上述代碼,其實是說,b() 的異步代碼可能會晚與 c() 去執(zhí)行,因為異步和兩個線程,導致代碼不再阻塞。
4.1.3 使用了 suspend 標注, 代碼的運行邏輯
-
a()運行結束后,b()開始運行; -
b()運行結束后「它的子線程也運行結束了」,c()才會開始運行;
a()->b(): a() 運行結束后 b() 執(zhí)行
b()->c(): b() 運行結束后 c() 執(zhí)行
可以看到使用了 suspend 標注的函數(shù),會使得當前代碼在該函數(shù)處處于等待它的完全運行結束。
而 suspend 掛起函數(shù)的完全運行結束是指:該函數(shù)中的所有代碼「可能包含一個新的子線程、」均運行結束。
上述三中不同的代碼的運行,其實是想告訴大家 suspend 這個關鍵字的作用是:
把原本異步的代碼,再次變得同步。
當天如果只是簡單的同步,那么肯定會有很多問題,
例如主線程等待子線程運行結束的問題,這是很不科學的,與我們把耗時操作放入子線程運行的初衷不符。
當然,協(xié)程當然不存在這種問題。它是如何解決的呢?
下面說一說協(xié)程的 「非阻塞掛起」
4.2 「非阻塞掛起」
我們還以第三種代碼情況說明, 不過這次加入了更多的代碼 test2() 方法。
假設完整代碼為:
b() 為 suspend 標注的掛起函數(shù), 其他為正常函數(shù)
以下為簡化代碼
fun mainTest() {
...
test()
test2() // 假設 test2() 運行在主線程
...
}
fun test() {
a()
b()
c()
}
fun test2() {
...
}
代碼實際的執(zhí)行運行邏輯為:
-
mainTest()中先執(zhí)行到test()方法,先運行a() -
a()運行結束后,「掛起函數(shù)」b()開始運行; - 「掛起函數(shù)」
b()的主線程代碼運行結束后,c()并不會運行,而是test2()開始運行, - 等到「掛起函數(shù)」
b()中開啟的子線程也運行結束后,c()才會開始運行;
圖示為:
mainTest()->test(): 先執(zhí)行 test() 「主線程」
test()->a(): 順序執(zhí)行 a() 「主線程」
a()->b(): a() 結束后,執(zhí)行掛起函數(shù) b() 「主線程」
b()-->test(): b() 中的主線程完成后,在切到子線程時,會標志 test() 執(zhí)行結束 「主線程」
test()-->mainTest(): test() 執(zhí)行結束,會順序執(zhí)行 test2(), 「主線程」
b()->c(): 注釋 1
注:上圖中的注釋 1 為:當掛起函數(shù)
b()里面的子線程運行結束后,會被協(xié)程切換到主線程,然后c()開始運行。
從上面可以看到 suspend 的作用是在當前代碼處 「1」暫停運行,轉而去運行該線程本身其他地方的邏輯代碼,等到該掛起函數(shù)中的代碼運行結束后「它里面的和它里面的子線程子協(xié)程均運行結束后」,才會在暫停處 「1」 繼續(xù)運行。
注: 上述代碼,其實并不完全成立,因為只能在「協(xié)程」或者「掛起函數(shù)」里面才可以調(diào)用「掛起函數(shù)」
b(), 因此test()并不成立,這里用于說明代碼運行邏輯,故而簡化了代碼。后面會給出完整的代碼。
哪里可以提現(xiàn)出:「非阻塞式掛起」這個含義呢?
就是因為在上面的代碼中,在 test() 中的 b() 處掛起時「本身為主線程」,并不會影響到主線程的執(zhí)行,因為 test2() 在主線程中為正常執(zhí)行,阻塞的只是該協(xié)程內(nèi)部的代碼。
4.3 附上完全測試代碼以及執(zhí)行結果
代碼為:
fun test {
viewModelScope.launch {
println("viewModelScope.launch ${Thread.currentThread()}")
mainTest()
println("viewModelScope.launch 結束了 ${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 線程結束" + Thread.currentThread())
}
println("33333 線程 是" + Thread.currentThread())
}
println("b() end end end" + Thread.currentThread())
}
運行結果為:
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] 「標注 1」
I/System.out: test2() doing doing doing Thread[main,5,main] 「標注 2」
I/System.out: 22222 線程結束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 結束了 Thread[main,5,main]
可以看到 test2() 的執(zhí)行是要早于 c() 方法的。
從運行結果上可以看到是和我們的分析一致的。
4.4 suspend b() 運行時的線程切換
從運行結果的 log 上, 我們還可以看到當前代碼執(zhí)行的線程信息。
我們發(fā)現(xiàn) suspend b() 的運行中,
-
b() start ...在主線程main中 - 通過
b()中的launch(IO)我們切換到了IO線程DefaultDispatcher-worker中 - 但是
b()中的子線程運行結束后,我們發(fā)現(xiàn)b() end再次回答了主線程main中
在上面的操作中,第三步中,我們并沒有顯示的調(diào)用切回主現(xiàn)場的代碼,我們卻回到了主線程。
由此說明:suspend 掛起函數(shù)在運行結束時會再次切換到原來的線程,真正的切換是有協(xié)程幫我們做的
值得一提的是,我們在上面說到
withContext()也具有自動切換原線程的功能。
因為……
withContext()本身就是一個「掛起函數(shù)」。
協(xié)程是怎么切換到原線程的呢?一家之言,我害怕說不清楚……慫
4.5 這里插入一個小的點。
根據(jù)上面,我們知道 suspend 標注的掛起函數(shù),協(xié)程會自動幫我們切換到原線程。
看兩行 log 信息
...
I/System.out: 22222 線程 是Thread[DefaultDispatcher-worker-2,5,main]
...
I/System.out: 22222 線程結束Thread[DefaultDispatcher-worker-9,5,main]
-
首先
Thread[DefaultDispatcher-worker-2,5,main]這三項分別是什么大部分人應該都知道,這是源碼
Thread.toString()方法中的返回值.
第一個參數(shù)DefaultDispatcher-worker-2代表的是當前線程的名字getName().
第二個參數(shù)5代表的是當前線程的優(yōu)先級getPriority()默認是5.
第三個參數(shù)main代表的是當前線程屬于哪個線程組。
-
為什么先后兩次線程會不一致?
在下面的
5部分CoroutineDispatcher我們會有介紹,IO調(diào)度器,它里面對應的是一個線程池。所以先后兩次線程名字不一樣。
但它們屬于同一線程池
**也屬于同一個調(diào)度器DefaultDispatcher**
帶來了一個問題,為什么在一個協(xié)程中,先后兩次線程的名字不同了呢?
肯定是在哪里切換了線程,才會導致線程的名稱不同。
看代碼中,我們知道:
22222 線程 是和22222 線程結束是在同一個launch(IO){}協(xié)程內(nèi)的;-
由于
delay()是個suspend掛起函數(shù),根據(jù)上面的4.4中的描述,協(xié)程在「掛起函數(shù)」運行完成后,自動幫我們切回原線程,但打印的結果表示其實在了另外一個線程中。所以更準確得說法是:
協(xié)程在「掛起函數(shù)」運行結束后,會自動切回原來的調(diào)度器中。
然后調(diào)度器可能會根據(jù)它對應的線程池,去選擇可用的線程繼續(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 小結
上面,我們使用了大量的代碼和邏輯圖,用于表示 suspend 在實際運行中起到的作用。
suspend會使得當前代碼的運行在該函數(shù)處「掛起「協(xié)程內(nèi)掛起」」。suspend的掛起,并不會影響主線程的代碼執(zhí)行,掛起的范圍也是我們上面提到的CoroutineScope這個范圍內(nèi)。suspend掛起函數(shù)具有在該函數(shù)運行結束后,再次切回原線程的能力。當然,這是協(xié)程內(nèi)部幫我們完成的。更準確的說法是:協(xié)程會在掛起函數(shù)運行結束后,自動切回原調(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()默認的調(diào)度器, 在
Android中對應的為「線程池」。
在新建的協(xié)程中,如果沒有指定dispatcher和ContinuationInterceptor則默認會使用該dispatcher。
線程池中會有多個線程。 -
Main: MainCoroutineDispatcher get() = MainDispatcherLoader.dispatcher在主線程「
UI線程」中的調(diào)度器。
只在主線程中, 單個線程。 Unconfined: CoroutineDispatcher = kotlinx.coroutines.Unconfined
-
IO: CoroutineDispatcher = DefaultScheduler.IO在
IO線程的調(diào)度器,里面的執(zhí)行邏輯會運行在IO線程, 一般用于耗時的操作。
對應的是「線程池」,會有多個線程在里面。IO和Default共享了線程。
6. 說一說協(xié)程里面常見的類
在文章的開頭,有一張圖,里面有一些在協(xié)程中涉及到的類,現(xiàn)在再來看一下。

是不是比剛在文章的開頭看上去要親和很多?
如果是,那么恭喜你,說明大部分內(nèi)容你都看到了,并且記在了心里,這么長且枯燥的內(nèi)容,很看到這里都很不容易。贊的贊的
6.1 CoroutineContext
CoroutineContext 和我們經(jīng)常在代碼中使用到的 Context 差別是很大的,它們兩沒有任何關系。
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 {
...
}
}
它的繼承關系是怎樣的呢?

什么是 Element? 什么是 Key?
Element是一個接口,實現(xiàn)了CoroutineContext,
代表著:CoroutineContext的一個元素,且為一個單例。而
Key是以Element作為 key 的接口。
從 CoroutineContext 需要根據(jù) Key 獲取到它對應的 Element
例如:
// 獲取當前協(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ù)的意思,當一個協(xié)程被創(chuàng)建時,就會有一個 Continuation 對應著該協(xié)程,它也可代表著協(xié)程的狀態(tài)。
用下面的圖表示協(xié)程的繼承關系:

我們可以發(fā)現(xiàn) Coroutine 繼承和實現(xiàn)了大量的接口,有 Job,Continuation, CoroutineScope
目前創(chuàng)建的協(xié)程,如果不特別指定,都是 StandaloneCoroutine 的實例,會立馬執(zhí)行。
當掛起后,需要重新執(zhí)行協(xié)程時,會調(diào)用 Continuation.resume() 再次得到該協(xié)程實例,然后開始調(diào)度運行。
7. 總結
一定要先說一句,一家之言,很多理解可能并不準確,有錯誤還請指正。
協(xié)程庫里面的元素太多了,上面我只是從使用的 API 接口入口,逐步介紹了涉及到的一些知識。
但協(xié)程里面的實現(xiàn)原理,調(diào)度器,切換原調(diào)度器的操作等原理,都未進行深入說明。
協(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),如果你有勇氣追求它