一些 API 啟動(dòng)長(zhǎng)時(shí)間運(yùn)行的操作(例如網(wǎng)絡(luò) IO、文件 IO、CPU 或 GPU 密集型任務(wù)等),并要求調(diào)用者阻塞直到它們完成。協(xié)程提供了一種避免阻塞線程并用更廉價(jià)、更可控的操作替代線程阻塞的方法:協(xié)程掛起。
在 Kotlin 1.1 中協(xié)程是實(shí)驗(yàn)性的。詳見(jiàn)下文
協(xié)程通過(guò)將復(fù)雜性放入庫(kù)來(lái)簡(jiǎn)化異步編程。程序的邏輯可以在協(xié)程中順序地表達(dá),而底層庫(kù)會(huì)為我們解決其異步性。該庫(kù)可以將用戶代碼的相關(guān)部分包裝為回調(diào)、訂閱相關(guān)事件、在不同線程(甚至不同機(jī)器!)上調(diào)度執(zhí)行,而代碼則保持如同順序執(zhí)行一樣簡(jiǎn)單。
許多在其他語(yǔ)言中可用的異步機(jī)制可以使用 Kotlin 協(xié)程實(shí)現(xiàn)為庫(kù)。這包括源于 C# 和 ECMAScript 的 async/await、源于 Go 的 管道 和 select 以及源于 C# 和 Python 生成器/yield。關(guān)于提供這些結(jié)構(gòu)的庫(kù)請(qǐng)參見(jiàn)其下文描述。
基本上,協(xié)程計(jì)算可以被掛起而無(wú)需阻塞線程。線程阻塞的代價(jià)通常是昂貴的,尤其在高負(fù)載時(shí),因?yàn)橹挥邢鄬?duì)少量線程實(shí)際可用,因此阻塞其中一個(gè)會(huì)導(dǎo)致一些重要的任務(wù)被延遲。
另一方面,協(xié)程掛起幾乎是無(wú)代價(jià)的。不需要上下文切換或者 OS 的任何其他干預(yù)。最重要的是,掛起可以在很大程度上由用戶庫(kù)控制:作為庫(kù)的作者,我們可以決定掛起時(shí)發(fā)生什么并根據(jù)需求優(yōu)化/記日志/截獲。
另一個(gè)區(qū)別是,協(xié)程不能在隨機(jī)的指令中掛起,而只能在所謂的掛起點(diǎn)掛起,這會(huì)調(diào)用特別標(biāo)記的函數(shù)。
當(dāng)我們調(diào)用標(biāo)記有特殊修飾符 suspend 的函數(shù)時(shí),會(huì)發(fā)生掛起:
suspend fun doSomething(foo: Foo): Bar {
……
}
這樣的函數(shù)稱為掛起函數(shù),因?yàn)檎{(diào)用它們可能掛起協(xié)程(如果相關(guān)調(diào)用的結(jié)果已經(jīng)可用,庫(kù)可以決定繼續(xù)進(jìn)行而不掛起)。掛起函數(shù)能夠以與普通函數(shù)相同的方式獲取參數(shù)和返回值,但它們只能從協(xié)程和其他掛起函數(shù)中調(diào)用。事實(shí)上,要啟動(dòng)協(xié)程,必須至少有一個(gè)掛起函數(shù),它通常是匿名的(即它是一個(gè)掛起 lambda 表達(dá)式)。讓我們來(lái)看一個(gè)例子,一個(gè)簡(jiǎn)化的 async() 函數(shù)(源自 kotlinx.coroutines 庫(kù)):
fun <T> async(block: suspend () -> T)
這里的 async() 是一個(gè)普通函數(shù)(不是掛起函數(shù)),但是它的 block 參數(shù)具有一個(gè)帶 suspend 修飾符的函數(shù)類型: suspend () -> T。所以,當(dāng)我們將一個(gè) lambda 表達(dá)式傳給 async() 時(shí),它會(huì)是掛起 lambda 表達(dá)式,于是我們可以從中調(diào)用掛起函數(shù):
async {
doSomething(foo)
……
}
繼續(xù)該類比,await() 可以是一個(gè)掛起函數(shù)(因此也可以在一個(gè) async {} 塊中調(diào)用),該函數(shù)掛起一個(gè)協(xié)程,直到一些計(jì)算完成并返回其結(jié)果:
async {
……
val result = computation.await()
……
}
更多關(guān)于 async/await 函數(shù)實(shí)際在 kotlinx.coroutines 中如何工作的信息可以在這里找到。
請(qǐng)注意,掛起函數(shù) await() 和 doSomething() 不能在像 main() 這樣的普通函數(shù)中調(diào)用:
fun main(args: Array<String>) {
doSomething() // 錯(cuò)誤:掛起函數(shù)從非協(xié)程上下文調(diào)用
}
還要注意的是,掛起函數(shù)可以是虛擬的,當(dāng)覆蓋它們時(shí),必須指定 suspend 修飾符:
interface Base {
suspend fun foo()
}
class Derived: Base {
override suspend fun foo() { …… }
}
@RestrictsSuspension 注解擴(kuò)展函數(shù)(和 lambda 表達(dá)式)也可以標(biāo)記為 suspend,就像普通的一樣。這允許創(chuàng)建 DSL 及其他用戶可擴(kuò)展的 API。在某些情況下,庫(kù)作者需要阻止用戶添加新方式來(lái)掛起協(xié)程。
為了實(shí)現(xiàn)這一點(diǎn),可以使用 @RestrictsSuspension 注解。當(dāng)接收者類/接口 R 用它標(biāo)注時(shí),所有掛起擴(kuò)展都需要委托給 R 的成員或其它委托給它的擴(kuò)展。由于擴(kuò)展不能無(wú)限相互委托(程序不會(huì)終止),這保證所有掛起都通過(guò)調(diào)用 R 的成員發(fā)生,庫(kù)的作者就可以完全控制了。
這在少數(shù)情況是需要的,當(dāng)每次掛起在庫(kù)中以特殊方式處理時(shí)。例如,當(dāng)通過(guò) buildSequence() 函數(shù)實(shí)現(xiàn)下文所述的生成器時(shí),我們需要確保在協(xié)程中的任何掛起調(diào)用最終調(diào)用 yield() 或 yieldAll() 而不是任何其他函數(shù)。這就是為什么 SequenceBuilder 用 @RestrictsSuspension 注解:
@RestrictsSuspension
public abstract class SequenceBuilder<in T> {
……
}
參見(jiàn)其 Github 上 的源代碼。
我們不是在這里給出一個(gè)關(guān)于協(xié)程如何工作的完整解釋,然而粗略地認(rèn)識(shí)發(fā)生了什么是相當(dāng)重要的。
協(xié)程完全通過(guò)編譯技術(shù)實(shí)現(xiàn)(不需要來(lái)自 VM 或 OS 端的支持),掛起通過(guò)代碼來(lái)生效?;旧?,每個(gè)掛起函數(shù)(優(yōu)化可能適用,但我們不在這里討論)都轉(zhuǎn)換為狀態(tài)機(jī),其中的狀態(tài)對(duì)應(yīng)于掛起調(diào)用。剛好在掛起前,下一狀態(tài)與相關(guān)局部變量等一起存儲(chǔ)在編譯器生成的類的字段中。在恢復(fù)該協(xié)程時(shí),恢復(fù)局部變量并且狀態(tài)機(jī)從剛好掛起之后的狀態(tài)進(jìn)行。
掛起的協(xié)程可以作為保持其掛起狀態(tài)與局部變量的對(duì)象來(lái)存儲(chǔ)和傳遞。這種對(duì)象的類型是 Continuation,而這里描述的整個(gè)代碼轉(zhuǎn)換對(duì)應(yīng)于經(jīng)典的延續(xù)性傳遞風(fēng)格(Continuation-passing style)。因此,掛起函數(shù)有一個(gè) Continuation 類型的額外參數(shù)作為高級(jí)選項(xiàng)。
關(guān)于協(xié)程工作原理的更多細(xì)節(jié)可以在這個(gè)設(shè)計(jì)文檔中找到。在其他語(yǔ)言(如 C# 或者 ECMAScript 2016)中的 async/await 的類似描述與此相關(guān),雖然它們實(shí)現(xiàn)的語(yǔ)言功能可能不像 Kotlin 協(xié)程這樣通用。
協(xié)程的設(shè)計(jì)是實(shí)驗(yàn)性的,這意味著它可能在即將發(fā)布的版本中更改。當(dāng)在 Kotlin 1.1 中編譯協(xié)程時(shí),默認(rèn)情況下會(huì)報(bào)一個(gè)警告:“協(xié)程”功能是實(shí)驗(yàn)性的。要移出該警告,你需要指定 opt-in 標(biāo)志。
由于其實(shí)驗(yàn)性狀態(tài),標(biāo)準(zhǔn)庫(kù)中協(xié)程相關(guān)的 API 放在 kotlin.coroutines.experimental 包下。當(dāng)設(shè)計(jì)完成并且實(shí)驗(yàn)性狀態(tài)解除時(shí),最終的 API 會(huì)移動(dòng)到 kotlin.coroutines,并且實(shí)驗(yàn)包會(huì)被保留(可能在一個(gè)單獨(dú)的構(gòu)件中)以實(shí)現(xiàn)向后兼容。
重要注意事項(xiàng):我們建議庫(kù)作者遵循相同慣例:給暴露基于協(xié)程 API 的包添加“experimental”后綴(如 com.example.experimental),以使你的庫(kù)保持二進(jìn)制兼容。當(dāng)最終 API 發(fā)布時(shí),請(qǐng)按照下列步驟操作:
com.example(沒(méi)有 experimental 后綴),這將最小化你的用戶的遷移問(wèn)題。
協(xié)程有三個(gè)主要組成部分:
kotlin.coroutines底層 API 相對(duì)較小,并且除了創(chuàng)建更高級(jí)的庫(kù)之外,不應(yīng)該使用它。 它由兩個(gè)主要包組成:
kotlin.coroutines.experimental 帶有主要類型與下述原語(yǔ)
kotlin.coroutines.experimental.intrinsics 帶有甚至更底層的內(nèi)在函數(shù)如 suspendCoroutineOrReturn關(guān)于這些 API 用法的更多細(xì)節(jié)可以在這里找到。
kotlin.coroutines 中的生成器 APIkotlin.coroutines.experimental 中僅有的“應(yīng)用程序級(jí)”函數(shù)是
這些包含在 kotlin-stdlib 中因?yàn)樗麄兣c序列相關(guān)。這些函數(shù)(我們可以僅限于這里的 buildSequence())實(shí)現(xiàn)了 生成器 ,即提供一種廉價(jià)構(gòu)建惰性序列的方法:
kotlin
import kotlin.coroutines.experimental.*
fun main(args: Array<String>) {
//sampleStart
val fibonacciSeq = buildSequence {
var a = 0
var b = 1
yield(1)
while (true) {
yield(a + b)
val tmp = a + b
a = b
b = tmp
}
}
//sampleEnd
// 輸出前五個(gè)斐波納契數(shù)字
println(fibonacciSeq.take(8).toList())
}這通過(guò)創(chuàng)建一個(gè)協(xié)程生成一個(gè)惰性的、潛在無(wú)限的斐波那契數(shù)列,該協(xié)程通過(guò)調(diào)用 yield() 函數(shù)來(lái)產(chǎn)生連續(xù)的斐波納契數(shù)。當(dāng)在這樣的序列的迭代器上迭代每一步,都會(huì)執(zhí)行生成下一個(gè)數(shù)的協(xié)程的另一部分。因此,我們可以從該序列中取出任何有限的數(shù)字列表,例如 fibonacciSeq.take(8).toList() 結(jié)果是 [1, 1, 2, 3, 5, 8, 13, 21]。協(xié)程足夠廉價(jià)使這很實(shí)用。
為了演示這樣一個(gè)序列的真正惰性,讓我們?cè)谡{(diào)用 buildSequence() 內(nèi)部輸出一些調(diào)試信息:
kotlin
import kotlin.coroutines.experimental.*
fun main(args: Array<String>) {
//sampleStart
val lazySeq = buildSequence {
print("START ")
for (i in 1..5) {
yield(i)
print("STEP ")
}
print("END")
}
// 輸出序列的前三個(gè)元素
lazySeq.take(3).forEach { print("$it ") }
//sampleEnd
}運(yùn)行上面的代碼看,是不是我們輸出前三個(gè)元素的數(shù)字與生成循環(huán)的 STEP 有交叉。這意味著計(jì)算確實(shí)是惰性的。要輸出 1,我們只執(zhí)行到第一個(gè) yield(i),并且過(guò)程中會(huì)輸出 START。然后,輸出 2,我們需要繼續(xù)下一個(gè) yield(i),并會(huì)輸出 STEP。3 也一樣。永遠(yuǎn)不會(huì)輸出再下一個(gè) STEP(以及END),因?yàn)槲覀冊(cè)僖矝](méi)有請(qǐng)求序列的后續(xù)元素。
為了一次產(chǎn)生值的集合(或序列),可以使用 yieldAll() 函數(shù):
kotlin
import kotlin.coroutines.experimental.*
fun main(args: Array<String>) {
//sampleStart
val lazySeq = buildSequence {
yield(0)
yieldAll(1..10)
}
lazySeq.forEach { print("$it ") }
//sampleEnd
}buildIterator() 的工作方式類似于 buildSequence(),但返回一個(gè)惰性迭代器。
可以通過(guò)為 SequenceBuilder 類寫(xiě)掛起擴(kuò)展(帶有上文描述的 @RestrictsSuspension 注解)來(lái)為 buildSequence() 添加自定義生產(chǎn)邏輯(custom yielding logic):
kotlin
import kotlin.coroutines.experimental.*
//sampleStart
suspend fun SequenceBuilder<Int>.yieldIfOdd(x: Int) {
if (x % 2 != 0) yield(x)
}
val lazySeq = buildSequence {
for (i in 1..10) yieldIfOdd(i)
}
//sampleEnd
fun main(args: Array<String>) {
lazySeq.forEach { print("$it ") }
}kotlinx.coroutines只有與協(xié)程相關(guān)的核心 API 可以從 Kotlin 標(biāo)準(zhǔn)庫(kù)獲得。這主要包括所有基于協(xié)程的庫(kù)可能使用的核心原語(yǔ)和接口。
大多數(shù)基于協(xié)程的應(yīng)用程序級(jí)API都作為單獨(dú)的庫(kù)發(fā)布:kotlinx.coroutines。這個(gè)庫(kù)覆蓋了
kotlinx-coroutines-core 的平臺(tái)無(wú)關(guān)異步編程select 和其他便利原語(yǔ)的類似 Go 的管道CompletableFuture 的 API:kotlinx-coroutines-jdk8kotlinx-coroutines-niokotlinx-coroutines-swing) 和 JavaFx (kotlinx-coroutines-javafx)kotlinx-coroutines-rx這些庫(kù)既作為使通用任務(wù)易用的便利的 API,也作為如何構(gòu)建基于協(xié)程的庫(kù)的端到端示例。