前面入門(mén)時(shí)講過(guò)一個(gè)最簡(jiǎn)單的例子,通過(guò) GlobalScope.launch { }
可以啟動(dòng)一個(gè)協(xié)程,GlobalScope
可以簡(jiǎn)單理解為協(xié)程構(gòu)造者,它實(shí)際上是接口 CoroutineScope
的子類讶坯,那我們來(lái)看看它到底是什么,啟動(dòng)一個(gè)協(xié)程需要哪些關(guān)鍵要素岗屏。接下來(lái)我們講講協(xié)程相關(guān)的幾個(gè)主要類辆琅,先混個(gè)臉熟,心里有個(gè)大體概念之后这刷,再逐步深入婉烟。
1. CoroutineScope介紹
顧名思義“協(xié)程域”,只有它能創(chuàng)建協(xié)程暇屋,既然是創(chuàng)建者似袁,同樣它能管理它所創(chuàng)建的協(xié)程。該接口定義如下:
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
}
接口定義很簡(jiǎn)單咐刨,只包含一個(gè)叫 CoroutineContext
的參數(shù)昙衅,我們稱之為協(xié)程上下文今妄,那么這又是個(gè)什么鬼焰手?我們應(yīng)該在很多地方都見(jiàn)過(guò)名叫上下文的東西,例如在 Android 中一個(gè) Activity 就是上下文 Context 的子類瞳秽,由此可以類推 CoroutineContext 包含了協(xié)程運(yùn)行時(shí)的一些信息联予,具體后面再逐步介紹啼县。我們?cè)倏纯?GlobalScope
的定義:
public object GlobalScope : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = EmptyCoroutineContext
}
原來(lái) GlobalScope
是個(gè)類似 Java 中的單例類,它的協(xié)程上下文是個(gè)空上下文 EmptyCoroutineContext
沸久。那么協(xié)程的啟動(dòng)方法是在哪里定義的呢季眷,接口里我們好像沒(méi)見(jiàn)到。原來(lái)協(xié)程的啟動(dòng)方法都是通過(guò)擴(kuò)展函數(shù)來(lái)定義的卷胯,它的方法簽名為:
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
從方法定義中可以看到子刮,協(xié)程的啟動(dòng)需要3個(gè)參數(shù):context(協(xié)程上下文)、start(協(xié)程啟動(dòng)模式)窑睁、block(協(xié)程體)挺峡,其中前2個(gè)參數(shù)都有默認(rèn)值,我們例子中的代碼其實(shí)只包含了協(xié)程體卵慰。協(xié)程上下文的概念很復(fù)雜沙郭,也特別難理解,我們可以將之類比為 Android 中的 Activity一樣裳朋。協(xié)程體就像 Thread.run()
方法中的代碼一樣病线,協(xié)程的運(yùn)行代碼都應(yīng)該寫(xiě)在里面吓著,這個(gè)很容易理解。該方法會(huì)返回一個(gè) Job
類型的對(duì)象送挑,有趣的是 Job
也是繼承自 CoroutineContext
绑莺,可以認(rèn)為協(xié)程就是一個(gè)任務(wù)。
2. CoroutineStart(啟動(dòng)模式)介紹
CoroutineStart 是個(gè)枚舉類惕耕,其定義如下:
public enum class CoroutineStart {
DEFAULT,
LAZY,
@ExperimentalCoroutinesApi
ATOMIC,
@ExperimentalCoroutinesApi
UNDISPATCHED;
}
共定義了4種啟動(dòng)模式纺裁,但是后2種還是帶有實(shí)驗(yàn)性質(zhì)的 Api,我們分別用代碼來(lái)演示它們之間的區(qū)別司澎。
2.1 DEFAULT
這是默認(rèn)的啟動(dòng)模式欺缘,一旦 launch
方法調(diào)用后,立即開(kāi)始調(diào)度協(xié)程的執(zhí)行挤安。這種模式有點(diǎn)像線程調(diào)用 Thread.start()
方法之后谚殊,系統(tǒng)開(kāi)始調(diào)度線程的執(zhí)行一樣。當(dāng)調(diào)度 OK 之后蛤铜,協(xié)程體里的代碼會(huì)立即執(zhí)行嫩絮。
//方便打印出代碼執(zhí)行所在線程
fun log(o: Any?) {
println("[${Thread.currentThread().name}]:$o")
}
GlobalScope.launch {
log(1)
val job = launch() {
log(2)
}
log(3)
}
運(yùn)行結(jié)果可能為:
[DefaultDispatcher-worker-1]:1
[DefaultDispatcher-worker-1]:3
[DefaultDispatcher-worker-1]:2
2.2 LAZY
懶加載模式,launch
方法調(diào)用后围肥,并不會(huì)立即調(diào)度協(xié)程的執(zhí)行剿干。需要手動(dòng)調(diào)用,該協(xié)程才會(huì)開(kāi)始調(diào)度執(zhí)行穆刻。
GlobalScope.launch {
log(1)
val job = launch(start = CoroutineStart.LAZY) {
log(2)
}
log(3)
}
同樣的代碼置尔,內(nèi)部的協(xié)程啟動(dòng)模式換成 LAZY
之后,再看執(zhí)行結(jié)果:
[DefaultDispatcher-worker-1]:1
[DefaultDispatcher-worker-1]:3
對(duì)比前面的代碼蛹批,能夠很明顯地看出 LAZY
與 DEFAULT
的差別撰洗。
我們修改代碼為:
GlobalScope.launch {
log(1)
val job = launch(start = CoroutineStart.LAZY) {
log(2)
}
job.join() //等待協(xié)程的執(zhí)行結(jié)果篮愉,這里會(huì)觸發(fā)協(xié)程的調(diào)度執(zhí)行
log(3)
}
運(yùn)行的結(jié)果為:
[DefaultDispatcher-worker-1]:1
[DefaultDispatcher-worker-1]:2
[DefaultDispatcher-worker-1]:3
2.3 ATOMIC
這種模式與 DEFAULT
類似腐芍,它也是一旦 launch
方法調(diào)用后,協(xié)程會(huì)立即開(kāi)始調(diào)度執(zhí)行试躏。但很有趣的是猪勇,在協(xié)程內(nèi)部沒(méi)有遇到掛起函數(shù)(suspend fun)之前,它不能取消掉颠蕴。
順便說(shuō)一下掛起函數(shù)泣刹,掛起函數(shù)是由 suspend
修飾的函數(shù),它只能在協(xié)程內(nèi)部或掛起函數(shù)內(nèi)調(diào)用犀被∫文可以簡(jiǎn)單理解為,它能"暫停"該函數(shù)的執(zhí)行寡键,當(dāng)然這里并不是真的暫停掀泳,只是說(shuō)協(xié)程調(diào)度器暫時(shí)不再調(diào)度該協(xié)程。
GlobalScope.launch {
log(1)
val job1 = launch(start = CoroutineStart.ATOMIC) {
log(2)
log(22)
}
job1.cancel()
val job2 = launch {
log(3)
log(33)
}
job2.cancel()
val job3 = launch(start = CoroutineStart.ATOMIC) {
log(4)
log(44)
delay(100)
log(444)
}
job3.cancel()
val job4 = launch(start = CoroutineStart.ATOMIC) {
delay(100)
log(5)
}
job4.cancel()
}
這段代碼的執(zhí)行結(jié)果為:
[DefaultDispatcher-worker-1]:1
[DefaultDispatcher-worker-3]:2
[DefaultDispatcher-worker-3]:22
[DefaultDispatcher-worker-2]:4
[DefaultDispatcher-worker-2]:44
共創(chuàng)建了4個(gè)協(xié)程:job1、job2员舵、job3脑沿、job4,其中協(xié)程job2為默認(rèn)啟動(dòng)模式马僻,其他的啟動(dòng)模式都為 ATOMIC
庄拇,delay(100)
是一個(gè)掛起函數(shù)調(diào)用,相當(dāng)于 Thread.sleep(100)
的作用韭邓,每個(gè)協(xié)程創(chuàng)建之后立即調(diào)用 cancel()
方法取消執(zhí)行措近。我們來(lái)分析每個(gè)結(jié)果:
- job1:協(xié)程體內(nèi)沒(méi)有調(diào)用掛起函數(shù),協(xié)程體內(nèi)的代碼都被執(zhí)行了女淑,該協(xié)程沒(méi)有被取消掉熄诡;
- job2:協(xié)程被取消掉了;
- job3:掛起函數(shù)
delay(100)
之前的代碼執(zhí)行了诗力,掛起函數(shù)后面的代碼沒(méi)有執(zhí)行凰浮; - job4:協(xié)程體內(nèi)的第一行代碼就是掛起函數(shù)調(diào)用,最終該協(xié)程體內(nèi)的代碼都沒(méi)執(zhí)行苇本;
從上面的例子中可以看到袜茧,DEFAULT
模式啟動(dòng)的協(xié)程如果還沒(méi)調(diào)度執(zhí)行是可以取消掉的,ATOMIC
模式啟動(dòng)的協(xié)程如果還沒(méi)調(diào)度執(zhí)行時(shí)就被取消瓣窄,協(xié)程體內(nèi)第一個(gè)掛起函數(shù)之前的代碼依舊會(huì)執(zhí)行笛厦。如果該協(xié)程內(nèi)部沒(méi)有調(diào)用任何掛起函數(shù),則該協(xié)程里的代碼無(wú)論如何也會(huì)執(zhí)行俺夕。協(xié)程的取消有點(diǎn)像線程的中斷一樣裳凸,suspend 函數(shù)又有點(diǎn)像線程里能夠拋出中斷異常的方法一樣。
2.4 UNDISPATCHED
這種模式具備 ATOMIC
的功能劝贸,與之不同的是姨谷,一旦調(diào)用 launch
方法后,該協(xié)程會(huì)立即在當(dāng)前線程執(zhí)行映九。
GlobalScope.launch {
log(1)
val job1 = launch(start = CoroutineStart.UNDISPATCHED) {
log(2)
delay(100)
log(22)
}
job1.cancel()
val job2 = launch(start = CoroutineStart.UNDISPATCHED) {
log(3)
delay(100)
log(33)
}
log("after job2")
val job3 = launch(start = CoroutineStart.ATOMIC) {
log(4)
delay(100)
log(44)
}
log("after job3")
}
執(zhí)行結(jié)果為:
[DefaultDispatcher-worker-2]:1
[DefaultDispatcher-worker-2]:2
[DefaultDispatcher-worker-2]:3
[DefaultDispatcher-worker-2]:after job2
[DefaultDispatcher-worker-2]:after job3
[DefaultDispatcher-worker-2]:4
[DefaultDispatcher-worker-2]:33
[DefaultDispatcher-worker-2]:44
job1 驗(yàn)證了它不能被取消的功能梦湘,job2 中 3
會(huì)立即在當(dāng)前線程執(zhí)行,所以 3
必然會(huì)在 after job2
之前執(zhí)行件甥,job3 中 4
會(huì)等待調(diào)度器調(diào)度執(zhí)行捌议,所以他并不會(huì)在 after job3
之前執(zhí)行,4
與 after job3
的執(zhí)行順序?qū)嵸|(zhì)上與協(xié)程的調(diào)度來(lái)決定引有。
3. CoroutineContext介紹
根據(jù)文檔里的說(shuō)明瓣颅,CoroutineContext 的概念主要有3點(diǎn):
- It is an indexed set of [Element] instances. 它是一個(gè)包含 Element 實(shí)例的索引集;
- An indexed set is a mix between a set and a map. 索引集是 set 和 map 的混合結(jié)構(gòu)譬正;
- Every element in this set has a unique [Key]. 這個(gè)集合中的每個(gè)元素都有一個(gè)唯一的 Key宫补;
說(shuō)的通俗一點(diǎn)僻孝,CoroutineContext 就是一個(gè)集合 Collection,這個(gè)集合既有 set 的特性又有 map 的特性守谓,集合里的元素都是 Element 類型的穿铆,每個(gè) Element 類型的元素都有一個(gè)類型為 Key 的鍵。按慣例先來(lái)看看類定義:
public interface CoroutineContext {
//操作符'[]'重載斋荞,通過(guò) Key 獲取 context 中的 Element 類型元素荞雏。可直接通過(guò) CoroutineContext[Key] 這種形式來(lái)獲取與 Key 關(guān)聯(lián)的元素平酿,類似從 List 中取出索引為 index 的某個(gè)元素:List[index]凤优,從 Map 中取出某個(gè)元素則為 Map.get(key)
public operator fun <E : Element> get(key: Key<E>): E?
//聚集函數(shù),函數(shù)式編程中出現(xiàn)比較多蜈彼,想象一下"菲波那切數(shù)列求和"就容易理解了
//這里是提供了遍歷當(dāng)前 context 中所有 Element 元素的能力
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
//操作符 '+' 重載筑辨,類似 List 中的 List.addAll(list)方法、Map 中的 Map.putAll(map) 方法幸逆,將2個(gè)集合合并成一個(gè)集合
public operator fun plus(context: CoroutineContext): CoroutineContext
//返回一個(gè)新的 context棍辕,但是該 conext 刪除了有指定 Key 的 Element。
public fun minusKey(key: Key<*>): CoroutineContext
//Key的定義还绘,空實(shí)現(xiàn)楚昭,僅僅只是做一個(gè)標(biāo)識(shí)
public interface Key<E : Element>
//Element的定義,同樣繼承自 CoroutineContext
public interface Element : CoroutineContext {
//每個(gè) Element 都有一個(gè) Key
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}
}
可以發(fā)現(xiàn)拍顷,CoroutineContext 感覺(jué)與 Java 里的 Map 最相似抚太,簡(jiǎn)直就是一個(gè)鍵為 Key 類型的 Map。眾所周知昔案,List尿贫、Map 的內(nèi)部數(shù)據(jù)結(jié)構(gòu)一般為數(shù)組、鏈表之類的踏揣,那么 CoroutineContext 的內(nèi)部數(shù)據(jù)結(jié)構(gòu)呢庆亡?
查看源碼,發(fā)現(xiàn)它的底層數(shù)據(jù)結(jié)構(gòu)是一個(gè)叫 CombinedContext
的類來(lái)實(shí)現(xiàn)的呼伸,這是一個(gè)內(nèi)部類身冀,定義如下:
internal class CombinedContext(
private val left: CoroutineContext,
private val element: Element
) : CoroutineContext, Serializable
它有2個(gè)參數(shù),left 為 CoroutineContext 類型括享,element 為就是集合里的元素≌浯伲看到這個(gè)定義是不是特奇怪铃辖,既不像數(shù)組又不像鏈表,那么它是怎么具備集合的功能的呢猪叙,為此我寫(xiě)了個(gè)簡(jiǎn)單的例子:
class List<E> constructor() {
private var head: E? = null
private var tail: List<E>? = null
constructor(head: E?, tail: List<E>?) : this() {
this.head = head
this.tail = tail
}
fun add(e: E) {
if (head == null) {
head = e
} else {
if (tail == null) {
val nextList = List<E>()
nextList.head = e
tail = nextList
} else {
tail?.add(e)
}
}
}
fun size(): Int = (if (head == null) 0 else 1) + (tail?.size() ?: 0)
}
據(jù)說(shuō)這種叫做 List 的遞歸定義娇斩,有些函數(shù)式編程語(yǔ)言中仁卷,就是采用這種方式來(lái)定義 List 的。它有點(diǎn)像鏈表犬第,又跟鏈表不太一樣锦积,CombinedContext
與之非常類似,僅僅是頭尾位置換了一下歉嗓,當(dāng)然它更復(fù)雜丰介,我們?cè)賮?lái)看 plus
方法的具體實(shí)現(xiàn):
public operator fun plus(context: CoroutineContext): CoroutineContext =
if (context === EmptyCoroutineContext) this else
context.fold(this) { acc, element ->
val removed = acc.minusKey(element.key)
if (removed === EmptyCoroutineContext) element else {
val interceptor = removed[ContinuationInterceptor]
if (interceptor == null) CombinedContext(removed, element) else {
val left = removed.minusKey(ContinuationInterceptor)
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}
整段代碼就是一遞歸的實(shí)現(xiàn),主要邏輯有:
- 除了少數(shù)情況外鉴分,主要返回的就是 CombinedContext 對(duì)象哮幢;
- 新返回的 CoroutineContext 對(duì)象,包含了 2 個(gè) context 里所包含的全部 Element 元素志珍;
- 在組合形成 CombinedContext 的時(shí)候橙垢,如果當(dāng)前 context 里有與要相加的 context 含有相同 Key 的 Element,則當(dāng)前 context 里的該元素會(huì)被刪除掉伦糯。這就讓 CoroutineContext 具備了 Set 的屬性柜某,一個(gè) Key,只能取出一個(gè)對(duì)應(yīng)的 Element敛纲;
- 這里有一個(gè)key為 ContinuationInterceptor 的元素莺琳,它也是繼承自 Element,通常叫做協(xié)程上下文攔截器(后面再單獨(dú)將它)载慈。它有點(diǎn)特殊惭等,不管多少次相加操作之后,它總是出現(xiàn)在最后面办铡。通過(guò)一個(gè) context辞做,我們總能最快找到攔截器(避免了遞歸查找);
下圖是主要的繼承了 CoroutineContext 的類圖:
下面我們來(lái)寫(xiě)個(gè)例子寡具,驗(yàn)證一下其中的特性:
val scope = MainScope()
val context = scope.coroutineContext
//取出 key 為 ContinuationInterceptor 的元素
println("interceptor: " + context[ContinuationInterceptor])
執(zhí)行結(jié)果為: interceptor: Main
class TestContext : ContinuationInterceptor {
override val key: CoroutineContext.Key<*> = ContinuationInterceptor
override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> {
return continuation
}
}
val scope = MainScope()
//執(zhí)行 context 的相加操作之后秤茅,再取出 key 為 ContinuationInterceptor 的元素
val context = scope.coroutineContext + TestContext()
println("interceptor: " + context[ContinuationInterceptor])
執(zhí)行結(jié)果為:interceptor: com.hjy.kotlinstudy.TestContext@18b8ff07
這里可以看到,context 的相加操作之后童叠,如果加號(hào)前后兩個(gè) context 都有相同的 key框喳,則最終只保留加號(hào)后面的 key 對(duì)應(yīng)的元素。如果這里你看到 context[ContinuationInterceptor]
方法調(diào)用厦坛,你一定會(huì)覺(jué)得很奇怪五垮,方括號(hào)里的參數(shù)應(yīng)該是一個(gè) Key 類型的對(duì)象啊,這里的 ContinuationInterceptor
只是一個(gè)繼承了 CoroutineContext
的接口啊杜秸,其實(shí)這只是 Kotlin 的一個(gè)特性放仗,在 ContinuationInterceptor 接口里定義了一個(gè)如下對(duì)象:
companion object Key : CoroutineContext.Key<ContinuationInterceptor>
這個(gè)俗稱伴生對(duì)象,context[ContinuationInterceptor]
等同于 context[ContinuationInterceptor.Key]
撬碟,在 kotlin 里直接寫(xiě)類名等同于該類里的伴生對(duì)象诞挨,以后看到類似的寫(xiě)法也就不會(huì)覺(jué)得晦澀難懂了莉撇。
4. 小結(jié)
本文介紹了與協(xié)程啟動(dòng)相關(guān)的幾個(gè)主要類,特別是 CoroutineContext惶傻,我認(rèn)為它是協(xié)程的核心概念棍郎,理解它有助于真正理解協(xié)程的內(nèi)部運(yùn)行機(jī)制。