本文主要介紹協(xié)程長什么樣子, 協(xié)程是什么東西, 協(xié)程掛起的實(shí)現(xiàn)原理以及整理了協(xié)程學(xué)習(xí)的資料.
協(xié)程 HelloWorld
協(xié)程在官方指南中被稱為一種輕量級的線程, 所以在介紹協(xié)程是什么東西之前, 這里通過幾個與線程對比的小例子初步認(rèn)識協(xié)程.
啟動線程與啟動協(xié)程
/* Kotlin code - Example 1.1 */
// 創(chuàng)建一條新線程并輸出 Hello World.
thread {
println("使用線程輸出 Hello World! Run in ${Thread.currentThread()}")
}
// 創(chuàng)建一個協(xié)程并使用協(xié)程輸出 Hello World.
GlobalScope.launch {
println("使用協(xié)程輸出 Hello World! Run in ${Thread.currentThread()}")
}
/* output */
使用線程輸出 Hello World! Run in Thread[Thread-0,5,main]
使用協(xié)程輸出 Hello World! Run in Thread[DefaultDispatcher-worker-1,5,main]
上面的例子是一個簡單的輸出 Hello World 的程序. 在這個例子中, 我們可以看到創(chuàng)建并啟動一條協(xié)程和創(chuàng)建并啟動一條線程的代碼幾乎一致, 唯一不同的就是創(chuàng)建線程調(diào)用的是 #thread
方法, 而創(chuàng)建協(xié)程調(diào)用的是 GlobalScope#launch
方法.
暫停線程與暫停協(xié)程
/* Kotlin code - Example 1.2 */
fun demoSleep() {
// 創(chuàng)建并運(yùn)行一條線程, 在線程中使用 Thread#sleep 暫停線程運(yùn)行 100ms.
thread {
val useTime = measureTimeMillis {
println("線程啟動")
println("線程 sleep 開始")
Thread.sleep(100L)
println("線程結(jié)束")
}
println("線程用時為 $useTime ms")
}
}
fun demoDelay() {
// 創(chuàng)建并運(yùn)行一條協(xié)程, 在協(xié)程中使用 #delay 暫停協(xié)程運(yùn)行 100 ms.
GlobalScope.launch {
val useTime = measureTimeMillis {
println("協(xié)程啟動")
println("協(xié)程 delay 開始")
delay(100L)
println("協(xié)程結(jié)束")
}
println("協(xié)程用時為 $useTime ms")
}
}
/* output */
線程啟動
線程 sleep 開始
線程結(jié)束
線程用時為 102 ms
協(xié)程啟動
協(xié)程 delay 開始
協(xié)程結(jié)束
協(xié)程用時為 106 ms
上面例子展示了暫停線程和暫停協(xié)程的方法. 我們可以使用 Thread#sleep
方法暫停一條線程, 而暫停一條協(xié)程, 只需要把 Thread#sleep
直接替換成 #delay
就可以了.
等待線程執(zhí)行結(jié)束與等待協(xié)程執(zhí)行結(jié)束
/* Kotlin code - Example 1.3 */
/**
* 線程等待另外一個線程任務(wù)完成的方法
*/
private fun waitOtherJobThread() {
// 啟動線程 A
thread {
println("線程 A: 啟動")
// 隨便定義一個變量用于阻塞線程 A
val waitThreadB = Object()
// 啟動線程 B
val threadB = thread {
println("線程 B: 啟動")
println("線程 B: 開始執(zhí)行任務(wù)")
for (i in 0..99) {
Math.E * Math.PI
}
println("線程 B: 結(jié)束")
}
// 線程 A 等待線程 B 完成任務(wù)
println("線程 A: 等待線程 B 完成")
threadB.join()
println("線程 A: 等待結(jié)束")
println("線程 A: 結(jié)束")
}
}
/**
* 協(xié)程等待另外一個協(xié)程任務(wù)完成的方法
*/
private fun waitOtherJobCoroutine() {
// 啟動協(xié)程 A
GlobalScope.launch {
println("協(xié)程 A: 啟動")
// 啟動協(xié)程 B
val coroutineB = GlobalScope.launch {
println("協(xié)程 B: 啟動")
println("協(xié)程 B: 開始執(zhí)行任務(wù)")
for (i in 0..99) {
Math.E * Math.PI
}
println("協(xié)程 B: 結(jié)束")
}
// 協(xié)程 A 等待協(xié)程 B 完成
println("協(xié)程 A: 等待協(xié)程 B 完成")
coroutineB.join()
println("協(xié)程 A: 等待結(jié)束")
println("協(xié)程 A: 結(jié)束")
}
}
/* output */
線程 A: 啟動
線程 A: 等待線程 B 完成
線程 B: 啟動
線程 B: 開始執(zhí)行任務(wù)
線程 B: 結(jié)束
線程 A: 等待結(jié)束
線程 A: 結(jié)束
協(xié)程 A: 啟動
協(xié)程 A: 等待協(xié)程 B 完成
協(xié)程 B: 啟動
協(xié)程 B: 開始執(zhí)行任務(wù)
協(xié)程 B: 結(jié)束
協(xié)程 A: 等待結(jié)束
協(xié)程 A: 結(jié)束
在上面的例子中, 創(chuàng)建了一條線程 A(協(xié)程 A), 然后在線程 A(協(xié)程 A)中再創(chuàng)建一條線程 B(協(xié)程 B), 接著使用 #join
方法使線程 A(協(xié)程 A)等待線程 B(協(xié)程 B)執(zhí)行結(jié)束. 我們可以清楚的看到等待線程和等待協(xié)程的代碼幾乎是一致的, 甚至連等待的方法都是 #join
.
中斷線程與中斷協(xié)程
/* Kotlin code - Example 1.4 線程的中斷與協(xié)程的中斷. */
private fun cancelThread() {
val job1 = thread {
println("線程: 啟動")
// 循環(huán)執(zhí)行 100 個耗時任務(wù).
for (i in 0..99) {
try {
Thread.sleep(50L)
println("線程: 正在執(zhí)行任務(wù) $i...")
} catch (e: InterruptedException) {
println("線程: 被中斷了")
break
}
}
println("線程: 結(jié)束")
}
// 延時 200ms 后中斷線程.
Thread.sleep(200L)
println("中斷線程!!!")
job1.interrupt()
}
private fun cancelCoroutine() = runBlocking {
val job1 = GlobalScope.launch {
println("協(xié)程: 啟動")
// 循環(huán)執(zhí)行 100 個耗時任務(wù).
for (i in 0..99) {
try {
delay(50L)
println("協(xié)程: 正在執(zhí)行任務(wù) $i...")
} catch (cancelException: CancellationException) {
println("協(xié)程: 被中斷了")
break
}
}
println("協(xié)程: 結(jié)束")
}
// 延時 200ms 后中斷協(xié)程.
delay(200L)
println("中斷協(xié)程!!!")
job1.cancel()
}
/* output */
線程: 啟動
線程: 正在執(zhí)行任務(wù) 0...
線程: 正在執(zhí)行任務(wù) 1...
線程: 正在執(zhí)行任務(wù) 2...
中斷線程!!!
線程: 被中斷了
線程: 結(jié)束
協(xié)程: 啟動
協(xié)程: 正在執(zhí)行任務(wù) 0...
協(xié)程: 正在執(zhí)行任務(wù) 1...
協(xié)程: 正在執(zhí)行任務(wù) 2...
中斷協(xié)程!!!
協(xié)程: 被中斷了
協(xié)程: 結(jié)束
在上面例子中, 可以看到中斷線程調(diào)用的方法是 #interrupt
, 當(dāng)線程被中斷后會拋出 InterruptedException
. 中斷協(xié)程的方法為 #cancel
. 協(xié)程被中斷后會拋出 CancellationException
.
通過上面的幾個小例子, 我們可以看到幾乎每一個線程的方法在協(xié)程中都有一個方法與之對應(yīng). 除了調(diào)用的方法名稱不一樣, 協(xié)程在使用上可以說幾乎和線程沒有特別大的區(qū)別.
協(xié)程是什么?
可掛起的計算實(shí)例。 它在概念上類似于線程苍柏,在這個意義上,它需要一個代碼塊運(yùn)行嫌松,并具有類似的生命周期 —— 它可以被創(chuàng)建與啟動皂冰,但它不綁定到任何特定的線程贿讹。它可以在一個線程中掛起其執(zhí)行,并在另一個線程中恢復(fù)伐谈。而且烂完,像 future 或 promise 那樣,它在完結(jié)時可能伴隨著某種結(jié)果(值或異常)诵棵。
上面這段話引用自 Kotlin 官方協(xié)程設(shè)計文檔中對協(xié)程的描述. 那么這段話應(yīng)該怎么理解呢? 首先, 協(xié)程需要一個計算實(shí)例. 類比與線程, 創(chuàng)建和啟動線程同樣需要一個計算實(shí)例. 對于線程來說, 線程的計算實(shí)例是 Runnable
, 我們需要把 Runnable
扔給線程才能在線程中完成計算任務(wù). 對于協(xié)程來說, 這個計算實(shí)例是 suspend
關(guān)鍵字修飾的方法或 lambda 表達(dá)式, 我們需要把這個 suspend
關(guān)鍵字修飾的方法或 lambda 表達(dá)式扔給協(xié)程才能在協(xié)程中完成計算任務(wù). 接著, 除了需要一個計算實(shí)例之外, 協(xié)程中的這個計算實(shí)例還必須是可掛起的, 這也是協(xié)程和線程的區(qū)別. 那么可掛起是什么意思呢? 比如在上面暫停線程與暫停協(xié)程的例子中, 線程和協(xié)程同樣是等待 100ms, 在線程的實(shí)現(xiàn)方式中, 是通過調(diào)用 Thread#sleep
方法阻塞線程來實(shí)現(xiàn)的, 而在協(xié)程的實(shí)現(xiàn)中, 調(diào)用 #delay
實(shí)現(xiàn)的等待是不會阻塞任何線程的(協(xié)程也是運(yùn)行在某一條線程上的). 同樣是等待, 線程等待的實(shí)現(xiàn)方式會阻塞線程, 而協(xié)程等待的實(shí)現(xiàn)方式不會阻塞線程, 所以就把線程的等待稱之為阻塞, 把協(xié)程的等待稱之為掛起. 同樣的, 在上面等待線程執(zhí)行結(jié)束與等待協(xié)程執(zhí)行結(jié)束例子中, 線程調(diào)用 threadB#join
勢必會造成線程 A 的阻塞, 而在協(xié)程中, 調(diào)用 coroutineB#join
也能實(shí)現(xiàn)同樣的功能卻不會造成任何線程的阻塞.
協(xié)程掛起的實(shí)現(xiàn)原理
經(jīng)過上文的簡單介紹, 我們知道了協(xié)程是什么, 協(xié)程和線程的區(qū)別是什么. 這里做個總結(jié), 協(xié)程是一個可掛起的計算實(shí)例, 和線程的區(qū)別就是協(xié)程的計算實(shí)例在執(zhí)行某些需要等待的任務(wù)時是可掛起的, 不阻塞線程的. 那么下面就開始介紹 Kotlin 協(xié)程是怎么實(shí)現(xiàn)等待某些任務(wù)而不阻塞線程的.
/* Kotlin code - Example 2.1*/
/**
* 自定義一個 delay 掛起函數(shù). 功能和協(xié)程庫中的 [delay] 函數(shù)是一樣的.
* 這里使用的是標(biāo)準(zhǔn)庫中定義掛起函數(shù)的方法.
*/
private suspend fun customDelay(delayInMillis: Long) = suspendCoroutine { complete: Continuation<Unit> ->
// 創(chuàng)建一個可延時執(zhí)行任務(wù)的 Thread Executor.
val executorService = Executors.newSingleThreadScheduledExecutor()
// 延時 delayInMillis ms 后調(diào)用 complete#resume 方法通知該任務(wù)已經(jīng)執(zhí)行完成了.
executorService.schedule({
complete.resume(Unit)
executorService.shutdown()
}, delayInMillis, TimeUnit.MILLISECONDS)
}
/**
* suspend 函數(shù)可以類比于 Thread 中的 Runnable.
* 同時, suspend 函數(shù)還被看作掛起點(diǎn), 也就是說運(yùn)行到這個函數(shù)的時候
* 可能會被切換到其他線程當(dāng)中運(yùn)行.
*/
private suspend fun doSomething() {
println("A")
customDelay(10L) // 掛起點(diǎn)
println("B")
customDelay(10L) // 掛起點(diǎn)
println("C")
}
/**
* Example 2.1 使用標(biāo)準(zhǔn)庫啟動一個協(xié)程.
*/
fun main() {
// 位于標(biāo)準(zhǔn)庫中的協(xié)程啟動函數(shù)
::doSomething.startCoroutine(Continuation(EmptyCoroutineContext) {
println(">>> doSomething Completed <<<")
})
// 防止進(jìn)程退出.
Thread.sleep(1000L)
}
/* output */
A
B
C
>>> doSomething Completed <<<
在正式介紹協(xié)程掛起原理之前, 需要先簡單介紹一下協(xié)程的幾個基本知識點(diǎn).
- 所有
suspend
修飾的函數(shù)或 lambda 表達(dá)式可以直接通過public fun <T> (suspend () -> T).startCoroutine(completion: Continuation<T>)
這個拓展方法創(chuàng)建并啟動協(xié)程, 該方法在suspend
修飾的計算實(shí)例(也就是前文提到的suspend
修飾的函數(shù)或 lambda 表達(dá)式)完成計算后會回調(diào)參數(shù)的#resumeWith
方法. 這個函數(shù)是最底層創(chuàng)建并啟動協(xié)程的函數(shù), 所有封裝的協(xié)程構(gòu)建器最終都要通過這個方法來創(chuàng)建并啟動一條協(xié)程. 在上面與線程對比的幾個小例子中使用到的GlobalScope#launch
協(xié)程構(gòu)建器最終也會調(diào)用該方法來創(chuàng)建并啟動協(xié)程. 這也是在協(xié)程是什么這一節(jié)提到協(xié)程需要的計算實(shí)例是suspend
關(guān)鍵字修飾的方法或 lambda 表達(dá)式的原因. -
suspend
修飾的函數(shù)被稱為掛起函數(shù). 調(diào)用掛起函數(shù)可能會掛起計算實(shí)例, 所以調(diào)用掛起函數(shù)的地方也被稱為掛起點(diǎn). 在上面代碼示例中,#customDelay
和#doSomething
都是掛起函數(shù). 在#doSomething
中調(diào)用#customDelay
的地方被稱為掛起點(diǎn). -
public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T
這個函數(shù)的作用是把被編譯器隱藏的Continuation
參數(shù)暴露出來. 我們可以通過這個函數(shù)自定義自己的掛起函數(shù), 實(shí)現(xiàn)等待卻不阻塞線程的任務(wù).
在弄懂這幾個知識點(diǎn)之后, 上面的代碼就很容易知道是干什么的了. 上面的代碼實(shí)際上就是使用 #doSomething
創(chuàng)建并啟動一條協(xié)程, 在 #doSomething
中依次輸出 "A", "B", "C". 執(zhí)行完 #doSomething
之后輸出 "doSomething Completed". 在執(zhí)行 #doSomething
的過程中會調(diào)用自定義的 #customDelay
方法掛起等待 10ms.
說了這么多, 那么協(xié)程到底是如何實(shí)現(xiàn)等待而不阻塞線程的呢? 這里面的原理其實(shí)十分簡單. 協(xié)程實(shí)現(xiàn)等待而不阻塞線程的方法就是通過回調(diào), 只不過這個回調(diào)是 Kotlin 編譯器實(shí)現(xiàn)的. 既然是編譯器實(shí)現(xiàn)的, 那么我們就需要反編譯一下這段代碼看看 Kotlin 編譯器到底做了什么黑科技的東西. 在 idea 中, Kotlin 編譯后的代碼可以通過 Tools -> Kotlin -> show kotlin bytecode 這幾個步驟查看. 為了更加清晰的展示編譯器干了什么東西, 這里我就直接貼我整理過后的反編譯 Java 代碼了. 下面這段整理過的 Java 代碼和反編譯的代碼是等效的.
/* Java code - Example 2.2 */
public class StartCoroutineSimulation {
/**
* 一個可掛起的計算實(shí)例. 根據(jù)協(xié)程的定義, 這個接口的對象就是協(xié)程.
*/
interface Continuation {
/**
* 喚醒被掛起的計算任務(wù), 繼續(xù)運(yùn)行.
*/
void resume();
}
/**
* 自定義一個 delay 掛起函數(shù). 功能和協(xié)程庫中的 [delay] 函數(shù)是一樣的.
* 這里使用的是標(biāo)準(zhǔn)庫中定義掛起函數(shù)的方法.
*/
private static void customDelay(Continuation complete, long delayInMillis) {
Continuation continuation = new Continuation() {
@Override
public void resume() {
// 創(chuàng)建一個可延時執(zhí)行任務(wù)的 Thread Executor.
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
// 延時 delayInMillis ms 后調(diào)用 complete#resume 方法通知該任務(wù)已經(jīng)執(zhí)行完成了.
executorService.schedule(() -> {
complete.resume();
executorService.shutdown();
}, delayInMillis, TimeUnit.MILLISECONDS);
}
};
continuation.resume();
}
private static void doSomething(Continuation complete) {
Continuation continuation = new Continuation() {
int label = 0;
@Override
public void resume() {
switch (label) {
case 0:
// 片段任務(wù) A
label = 1;
System.out.println("A");
customDelay(this, 10L);
return;
case 1:
// 片段任務(wù) B
label = 2;
System.out.println("B");
customDelay(this, 10L);
return;
case 2:
// 片段任務(wù) C
label = 3;
System.out.println("C");
break;
}
complete.resume();
}
};
continuation.resume();
}
/**
* Example 2.2 模擬 kotlin 協(xié)程標(biāo)準(zhǔn)庫啟動一個協(xié)程.
*/
public static void main(String[] args) {
doSomething(new Continuation() {
@Override
public void resume() {
System.out.println(">>> doSomething Completed <<<");
}
});
}
}
/* output */
A
B
C
>>> doSomething Completed <<<
通過反編譯的代碼, 我們可以看出 Kotlin 編譯器做了以下幾個點(diǎn).
- 每一個掛起函數(shù)都被編譯成了一個
Continuation
. - 每一個掛起函數(shù)都被編譯器添加了一個
Continuation
參數(shù). 在完成該函數(shù)的任務(wù)之后, 會回調(diào)該參數(shù)的#resume
方法. 該參數(shù)在 Kotlin 的源碼中是被隱藏的, 所以自定義掛起函數(shù)的時候需要使用public suspend inline fun <T> suspendCoroutine(crossinline block: (Continuation<T>) -> Unit): T
函數(shù)把隱藏的Continuation
參數(shù)暴露出來, 以便通知調(diào)用者任務(wù)已經(jīng)完成了. - 如果掛起函數(shù)中有掛起點(diǎn), 被編譯成的
Continuation
中的#resume
方法會被實(shí)現(xiàn)成狀態(tài)機(jī)模式. 兩兩掛起點(diǎn)之間組成一種狀態(tài). 在上面例子中, 我們可以清晰的看到println("A")
到第一個#costomDelay
方法之間組成了第一種狀態(tài),println("B")
到第二個#customDelay
方法之間組成了第二種狀態(tài), 最后的println("C")
組成最后一種狀態(tài). - 在一個掛起函數(shù)調(diào)用另外一個掛起函數(shù)時, 需要把自身作為參數(shù)傳入另外一個掛起函數(shù)中. 當(dāng)另外一個掛起函數(shù)完成時, 會回調(diào)參數(shù)的
#resume
方法通知調(diào)用者繼續(xù)完成任務(wù). 例如在上面的例子中, 從#doSomething
函數(shù)調(diào)用#customDelay
函數(shù)對應(yīng)代碼customDelay(this, 10L);
中可以看出#doSomething
把自身this
作為參數(shù)傳給了#customDelay
方法, 而最終自定義的#customDelay
方法在等待任務(wù)結(jié)束后通過complete.resume();
這句代碼讓#doSomething
函數(shù)繼續(xù)運(yùn)行. 順帶一提, Kotlin 規(guī)定suspend
修飾的函數(shù)或 lambda 表達(dá)式只能在suspend
修飾的函數(shù)或表達(dá)式中調(diào)用. 就是因?yàn)?suspend
修飾的函數(shù)或 lambda 表達(dá)式會編譯成需要Continuation
參數(shù)的Continuation
, 調(diào)用另外一個suspend
修飾的函數(shù)或 lambda 表達(dá)式需要傳入一個Continuation
作為參數(shù), 而只有在suspend
修飾的函數(shù)或 lambda 表達(dá)式中才有Continuation
(自身) 對象傳入另外一個suspend
函數(shù)中.
至此, 我們已經(jīng)清晰的了解到了協(xié)程是怎么實(shí)現(xiàn)等待而不阻塞線程的了. 總結(jié)成一句話就是 suspend
關(guān)鍵字修飾的方法或 lambda 表達(dá)式會編譯成一個帶 Continuation
參數(shù)的 Continuation
對象, 當(dāng)一個 Continuation
調(diào)用另外一個 Continuation
時需要把自身作為參數(shù)傳入到另外一個 Continuation
中, 另外一個 Continuation
完成任務(wù)后會傳入的 Continuation
參數(shù)的 #resume
方法讓調(diào)用者繼續(xù)運(yùn)行. 更簡單的說, 協(xié)程的掛起就是通過 Continuation
這個回調(diào)對象實(shí)現(xiàn)的.
協(xié)程的使用指導(dǎo)
本片文章的初衷就是讓大家初步認(rèn)識一下協(xié)程長什么樣子, 協(xié)程是什么東西, 協(xié)程的掛起原理是什么. 搞明白了這個三個問題, 那么本片文章的目的也就達(dá)到了. 如果有興趣繼續(xù)學(xué)習(xí) Kotlin 協(xié)程相關(guān)內(nèi)容, 這里整理了一些 Kotlin 協(xié)程相關(guān)的官方文檔資料. 資料按易難程度順序排列.
- 協(xié)程的基本使用 - 來自 Kotlin 官方文檔
- 把 Callback 回調(diào)轉(zhuǎn)換成協(xié)程 - 來自 Kotlin 官方協(xié)程設(shè)計文檔 (理解該文章需要讀懂上文協(xié)程掛起的實(shí)現(xiàn)原理)
- 使用協(xié)程的方式實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模式 - 來自 Kotlin 官方文檔
- 使用協(xié)程進(jìn)行 UI 編程指南 - 來自 Kotlin 官方協(xié)程 UI 編程指導(dǎo)
- 像 RxJava 一樣使用協(xié)程 - 來自 Kotlin 官方文檔
最后的最后, 如果覺得本文對你有幫助, 請幫我點(diǎn)個??. 謝謝大家 ^ _ ^. 本文歡迎分享和轉(zhuǎn)載, 轉(zhuǎn)載請補(bǔ)上鏈接并注明出處.