這篇文章會介紹 Kotlin 協(xié)程各個部分的作用,和常見類/函數(shù)/對象的用法。
從一個最簡單的例子看起:
fun main() {
GlobalScope.launch {
println("協(xié)程中的線程是:" + Thread.currentThread().name)
}
// 等待1秒鐘,讓協(xié)程執(zhí)行完
Thread.sleep(1000)
}
這段代碼是在協(xié)程中打印所在線程的名稱,其中 GlobalScope 是協(xié)程默認(rèn)的全局作用域,launch 是一個協(xié)程構(gòu)造器,它創(chuàng)建了一個協(xié)程并自動執(zhí)行。下面我們詳細(xì)了解下協(xié)程的這三個部分:
(1) 協(xié)程作用域(CoroutineScope);
(2) 協(xié)程構(gòu)造器;
(3) 協(xié)程上下文(CoroutineContext) 和調(diào)度器(CoroutineDispatcher);
1. 協(xié)程作用域
例子中的 GlobalScope 雖然首字母大寫,但它是一個單例對象,是默認(rèn)的全局作用域。
GlobalScope 實現(xiàn)了 CoroutineScope 接口,這個接口持有了協(xié)程上下文,定義如下:
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
1.1 協(xié)程作用域的作用
作用域的主要作用是滿足結(jié)構(gòu)化并發(fā)的需求。
每個線程啟動后,它執(zhí)行的上下文就是整個進(jìn)程,沒有線程獨立的作用域和任務(wù)邊界。
但對于協(xié)程,我們很少需要一個全局的協(xié)程,協(xié)程總是與應(yīng)用程序中的某個局部作用域相關(guān),這個局部作用域是一個生命周期有限的實體,例如一次網(wǎng)絡(luò)加載、一個 Activity 或 Fragment。
想更好地理解什么是「結(jié)構(gòu)化并發(fā)」可以看這篇文章:《什么是結(jié)構(gòu)化并發(fā) 》。
1.2 如何自定義作用域?
協(xié)程作用域的創(chuàng)建方式有很多,常見的有:
① 繼承 CoroutineScope 接口自己實現(xiàn);
② 使用 coroutineScope 方法創(chuàng)建;
③ 使用 supervisorScope 方法創(chuàng)建;
下面分別看看:
(1) 繼承 CoroutineScope 接口實現(xiàn)自定義的作用域
如果你有一個業(yè)務(wù)相對獨立的類,你可以繼承 CoroutineScope 接口,使得你這個類成為一個協(xié)程的作用域。
例如 Android 開發(fā)中每一個 Activity 都可以是一個作用域:
class MainActivity: AppCompatActivity(), CoroutineScope {
/** 需要實現(xiàn)協(xié)程上線文,這里使用空上下文 */
override val coroutineContext = EmptyCoroutineContext
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 在默認(rèn)的子線程中,請求網(wǎng)絡(luò)數(shù)據(jù)
launch {
val res = requestService()
// 在主線程中,更新 UI
launch(Dispatchers.Main) {
updateUi(res)
}
}
}
override fun onDestroy() {
super.onDestroy()
// 在 Activity 銷毀時取消協(xié)程
cancel()
}
}
為了在 Android/JavaFx 等場景中更方便的使用,官方提供了 MainScope() 方法快速創(chuàng)建基于主線程的協(xié)程作用域。
只需要將 CoroutineScope 的實現(xiàn)通過 by 關(guān)鍵字委托給 MainScope 對象即可:
(2) 使用 MainScope 創(chuàng)建協(xié)程作用域
class MainActivity: AppCompatActivity(), CoroutineScope by MainScope() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 在IO線程中,請求網(wǎng)絡(luò)數(shù)據(jù)
launch(Dispatchers.IO) {
val res = requestService()
// 在主線程中,更新 UI
launch {
updateUi(res)
}
}
}
override fun onDestroy() {
super.onDestroy()
// 在 Activity 銷毀時取消
cancel()
}
}
(3) 使用 coroutineScope 和 supervisorScope 方法創(chuàng)建協(xié)程作用域
coroutineScope 方法可以用來創(chuàng)建一個子作用域,它只能在另一個已有的協(xié)程作用域中調(diào)用,例如在另外一個 suspend 方法中調(diào)用。
supervisorScope 方法和 coroutineScope 類似,也用于創(chuàng)建一個子作用域,
區(qū)別是 supervisorScope 出現(xiàn)異常時不影響其他子協(xié)程, coroutineScope 出現(xiàn)異常時會把異常拋出。
篇幅所限,不舉例子了。
2. 協(xié)程構(gòu)造器
例子中的 launch 就是一個攜程構(gòu)造器,利用協(xié)程構(gòu)造器可以方便地構(gòu)造出一個協(xié)程對象。
在創(chuàng)建線程時,我們可以這樣創(chuàng)建:
import kotlin.concurrent.thread
thread {
// 在子線程中執(zhí)行的代碼
}
同樣,創(chuàng)建協(xié)程時,我們可以利用 launch、async、runBlocking、withContext 等構(gòu)造器創(chuàng)建協(xié)程,例如:
GlobalScope.launch {
// 在協(xié)程中執(zhí)行的代碼
}
2.1 launch
launch 是最常見的協(xié)程構(gòu)建器,它會啟動一個新的協(xié)程(AbstractCoroutine),并將這個協(xié)程對象返回,接著會在協(xié)程中執(zhí)行參數(shù)中的 block。
AbstractCoroutine 繼承了 Job,launch 返回的 Job 對象實際就是協(xié)程對象本身。
launch 的原型如下:
public fun CoroutineScope.launch(
/** 上下文 */
context: CoroutineContext = EmptyCoroutineContext,
/** 如何啟動 */
start: CoroutineStart = CoroutineStart.DEFAULT,
/** 啟動后要執(zhí)行的代碼 */
block: suspend CoroutineScope.() -> Unit
): Job
launch 方法有兩個可選參數(shù):CoroutineContext 和 CoroutineStart。
CoroutineContext:
是協(xié)程的上下文,默認(rèn)使用 EmptyCoroutineContext,作用是決定把協(xié)程派發(fā)到哪個線程中執(zhí)行,下文會介紹。
CoroutineStart:
是啟動時刻的枚舉,默認(rèn)使用 CoroutineStart.DEFAULT,表示盡快執(zhí)行。
2.2 async
async 比較常見,它也會啟動新的協(xié)程(AbstractCoroutine),并返回這個協(xié)程對象,然后在協(xié)程中執(zhí)行 block。
返回類型 Deferred 繼承自 Job,與 Job 的區(qū)別是 Job 不會攜帶返回值, Deferred 帶了返回值。
所以 async 多用于需要返回結(jié)果的場景。
async 的函數(shù)原型:
public fun <T> CoroutineScope.async(
/** 上下文 */
context: CoroutineContext = EmptyCoroutineContext,
/** 如何啟動 */
start: CoroutineStart = CoroutineStart.DEFAULT,
/** 啟動后要執(zhí)行的代碼 */
block: suspend CoroutineScope.() -> Unit
): Deferred<T>
參數(shù)和 launch 一樣,我們看看 async 怎么獲取返回值:
// 任務(wù)1:耗時一秒后返回100
val coroutine1 = GlobalScope.async {
delay(1000)
return@async 100
}
// 任務(wù)2:耗時1秒后返回200
val coroutine2 = GlobalScope.async {
delay(1000)
return@async 200
}
// 上面兩個協(xié)程會并發(fā)執(zhí)行
// 等待兩個任務(wù)都執(zhí)行完畢后,再繼續(xù)下一步(打印結(jié)果)。
GlobalScope.launch {
val v1 = coroutine1.await()
val v2 = coroutine2.await()
log("執(zhí)行的結(jié)果,v1 = $v1, v2=$v2")
}
async 還有更多用法,后面會介紹。
2.3 runBlocking
runBlocking 會啟動新的協(xié)程(AbstractCoroutine),并返回這個協(xié)程對象,然后在協(xié)程中執(zhí)行 block。
與 launch 和 async 不同的是,runBlocking 會阻塞住當(dāng)前線程,直到 block 執(zhí)行完畢。
runBlocking 不應(yīng)該在協(xié)程中調(diào)用,它大多數(shù)使用場景是為了測試。
runBlocking 的函數(shù)原型:
public fun <T> runBlocking(
context: CoroutineContext = EmptyCoroutineContext,
block: suspend CoroutineScope.() -> T
): T
2.4 withContext
withContext 會在調(diào)用處掛起,直到 block 執(zhí)行完畢。它需要指定一個協(xié)程上下文,block 會在這個上下文中執(zhí)行。
withContext 的函數(shù)原型:
public suspend fun <T> withContext(
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
使用示例:
runBlocking {
launch {
delay(1000)
log("第 1 處的代碼執(zhí)行完畢!")
}
withContext(GlobalScope.coroutineContext) {
log("第 2 處的代碼開始執(zhí)行!")
delay(2000)
log("第 2 處的代碼執(zhí)行完畢!")
}
log("外部代碼執(zhí)行完畢!")
}
上面代碼的輸出結(jié)果是:
[Thread: 1 ] [12:32:23.772] 協(xié)程 2 中的代碼開始執(zhí)行!
[Thread: 1 ] [12:32:24.784] 協(xié)程 1 中的代碼執(zhí)行完畢!
[Thread: 1 ] [12:32:25.779] 協(xié)程 2 中的代碼執(zhí)行完畢!
[Thread: 1 ] [12:32:25.779] 外部代碼執(zhí)行完畢!
可以看到,withContext 調(diào)用后,后續(xù)的代碼會等到 withContext 的 block 執(zhí)行完畢后再執(zhí)行。 同時,線程是沒有被阻塞的。
3. 協(xié)程上下文 和 調(diào)度器
CoroutineContext 是一個接口,定義協(xié)程的上下文。
CoroutineDispatcher 負(fù)責(zé)決定將協(xié)程放到哪個線程中去執(zhí)行。
調(diào)度器(CoroutineDispatcher) 是上下文(CoroutineContext) 的子類。
Kotlin 提供了默認(rèn)的幾個調(diào)度器,放在 Dispatchers 中,他們分別是:
3.1 Dispatchers 中包含的調(diào)度器
(1) Dispatchers.Default
默認(rèn)的派發(fā)器。它會使用后臺共享的線程池。
(2) Dispatchers.Main
使用主線程。只有在 Android、JavaFx 等平臺才有。需要引入對應(yīng)的依賴:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5'
(3) Dispatchers.Unconfined
這個協(xié)程派發(fā)器會在調(diào)用者線程內(nèi)啟動協(xié)程, 但只會持續(xù)運行到第一次掛起點為止。
在掛起之后, 它會在哪個線程內(nèi)恢復(fù)執(zhí)行, 完全由被調(diào)用的掛起函數(shù)來決定。
非受限派發(fā)器(Unconfined dispatcher) 適用的場景是, 協(xié)程不占用 CPU 時間,
也不更新那些限定于某個特定線程的共享數(shù)據(jù)(比如 UI).
(4) Dispatchers.IO
用于執(zhí)行 IO 類型的協(xié)程。它會和 Dispatchers.Default 共享線程池。
所以在一個 Dispatchers.Default 的作用域中,試圖切換到 Dispatchers.IO 中執(zhí)行,線程不一定會切換。例如:
除了繼承了 上下文(CoroutineContext) 的調(diào)度器,還有幾個常見的 CoroutineContext 子類:
3.2 EmptyCoroutineContext
空上下文。
launch等協(xié)程構(gòu)造器使用的默認(rèn)上下文就是這個。
當(dāng)使用這個對象時,表示當(dāng)前的 block 執(zhí)行在父協(xié)程的上下文。
例如:
fun main() {
runBlocking(context = ctx) {
// 這里沒有指定上下文,默認(rèn)使用 EmptyCoroutineContext,
// 也就是使用 runBlocking 的上下文 ctx
GlobalScope.launch {}
}
}
如果沒有父協(xié)程,會新創(chuàng)建一個線程池,例如:
fun main () {
// 外層沒有父協(xié)程,且使用 EmptyCoroutineContext,
// 則內(nèi)部會新建一個線程池
GlobalScope.launch {}
}
3.3 newSingleThreadContext
當(dāng)使用這個參數(shù)時,表示當(dāng)前 block 執(zhí)行在新的協(xié)程上下文中,例如:
fun main () {
val ctx = newSingleThreadContext("新的協(xié)程")
GlobalScope.launch(context = ctx) {
log("在新協(xié)程中執(zhí)行")
// 不再使用時,需要手動 close 掉,節(jié)省線程資源
ctx.close()
}
}
注意:我們手動創(chuàng)建的協(xié)程上下文,一定要在不用時 close 掉。