使用協程需要引入
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}
1.什么是協程
官方文檔(本質上,協程是輕量級的線程。)
import kotlinx.coroutines.*
fun main() {
GlobalScope.launch { // 在后臺啟動一個新的協程并繼續(xù)
delay(1000L) // 非阻塞的等待 1 秒鐘(默認時間單位是毫秒)
println("World!") // 在延遲后打印輸出
}
println("Hello,") // 協程已在等待時主線程還在繼續(xù)
Thread.sleep(2000L) // 阻塞主線程 2 秒鐘來保證 JVM 存活
}
個人理解:協程是一個線程框架布蔗,協程就是方法調用封裝成類線程的API强戴。
使用協程
啟動
協程需要運行在協程上下文環(huán)境歉摧,在非協程環(huán)境中憑空啟動協程燃异,有三種方式
runBlocking{}
啟動一個新協程礼饱,并阻塞當前線程作郭,直到其內部所有邏輯及子協程邏輯全部執(zhí)行完成哨查。
該方法的設計目的是讓suspend風格編寫的庫能夠在常規(guī)阻塞代碼中使用逗抑,常在main方法和測試中使用。
GlobalScope.launch{}
在應用范圍內啟動一個新協程寒亥,協程的生命周期與應用程序一致邮府。這樣啟動的協程并不能使線程保活溉奕,就像守護線程褂傀。
由于這樣啟動的協程存在啟動協程的組件已被銷毀但協程還存在的情況,極限情況下可能導致資源耗盡腐宋,因此并不推薦這樣啟動紊服,尤其是在客戶端這種需要頻繁創(chuàng)建銷毀組件的場景。
CoroutineScope + launch{}
這是在應用中最推薦使用的協程使用方式——為自己的組件實現CoroutieScope接口胸竞,在需要的地方使用launch{}方法啟動協程欺嗤。使得協程和該組件生命周期綁定,組件銷毀時卫枝,協程一并銷毀煎饼。從而實現安全可靠地協程調用。
在一個協程中啟動子協程校赤,一般來說有兩種方式
launch{}
異步啟動一個子協程
async{}
異步啟動一個子協程吆玖,并返回Deffer對象,可通過調用Deffer.await()方法等待該子協程執(zhí)行完成并獲取結果马篮,常用于并發(fā)執(zhí)行-同步等待的情況
下面舉個栗子
class Test CoroutineScope(): CoroutineScope {
override fun getList(handler: Handler<Result<Response>>){
launch{
val deffer1 = async{ awaitResult<List<JsonObject>>{ dbService.getContentList(it) } }
val deffer2 = async{ awaitResult<List<JsonObject>>{ dbService.getAuthorList(it) } }
val contents = deffer1.await()
val authors = deffer2.await()
val reuslt = contents.map{ content ->
content.put("author", authors.filter{ ... }.first())
}
resultHandler.succeed(reuslt)
}
}
}
協程的取消
launch{}返回Job沾乘,async{}返回Deffer,Job和Deffer都有cancel()取消協程浑测。
取消自協程不影響父協程翅阵,取消父協程,子協程也取消迁央。
從協程內部看取消的效果
- 標準庫的掛起方法會拋出CancellationException異常掷匠。
- 用戶自定義的常規(guī)邏輯并不會收到影響,除非我們手動檢測isActive標志岖圈。
一個栗子
val job = launch {
// 如果這里不檢測isActive標記讹语,協程就不會被正常cancel,而是執(zhí)行直到正常結束
while (isActive) {
......
}
}
job.cancelAndJoin()
學習了啟動跟取消蜂科,來看看協程異常顽决。
異常
Kotlin協程的異常有兩種
- 因協程取消短条,協程內部suspend方法拋出的CancellationException
- 常規(guī)異常,這類異常才菠,有兩種異常傳播機制
- launch:將異常自動向父協程拋出慌烧,將會導致父協程退出
- async: 將異常暴露給用戶(通過捕獲deffer.await()拋出的異常)
上一個官方栗子
fun main() = runBlocking {
val job = GlobalScope.launch { // root coroutine with launch
println("Throwing exception from launch")
throw IndexOutOfBoundsException() // 我們將在控制臺打印 Thread.defaultUncaughtExceptionHandler
}
job.join()
println("Joined failed job")
val deferred = GlobalScope.async { // root coroutine with async
println("Throwing exception from async")
throw ArithmeticException() // 沒有打印任何東西,依賴用戶去調用等待
}
try {
deferred.await()
println("Unreached")
} catch (e: ArithmeticException) {
println("Caught ArithmeticException")
}
}
控制臺輸出
Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException
Joined failed job
Throwing exception from async
Caught ArithmeticException
全局異常處理(CoroutineExceptionHandler)
launch(CoroutineExceptionHandler { _, e ->
logger.error("Exception when get content list.", e)
resultHandler.fail()
}) {
val deffer1 = async{ awaitResult<List<JsonObject>>{ dbService.getContentList(it) } }
val deffer2 = async{ awaitResult<List<JsonObject>>{ dbService.getAuthorList(it) } }
val contents = deffer1.await()
val authors = deffer2.await()
val reuslt = contents.map{ content ->
content.put("author", authors.filter{ ... }.first())
}
}
協程上下文
顧名思義鸠儿,協程上下文表示協程的運行環(huán)境,包括協程調度器厕氨、代表協程本身的Job进每、協程名稱、協程ID等命斧。通過CoroutineContext定義田晚,CoroutineContext被定義為一個帶索引的集合,集合的元素為Element国葬,上面所提到調度器贤徒、Job等都實現了Eelement接口。
由于CoroutineContext被定義為集合汇四,因此在實際使用時可以自由組合加減各種上下文元素接奈。
啟動子協程時,子協程默認會繼承除Job外的所有父協程上下文元素通孽,創(chuàng)建新的Job序宦,并將父Job設置為當前Job的父親。
啟動子協程時背苦,可以指定協程上下文元素互捌,如果父上下文中存在該元素則覆蓋,不存在則添加行剂。
// 自定義新協程名稱
launch(CoroutineName("customName")){
... ...
}
調度器
調度器是協程上下文中眾多元素中最重要的一個秕噪,通過CoroutineDispatcher定義,它控制了協程以何種策略分配到哪些線程上運行厚宰。這里介紹幾種常見的調度器
Dispatcher.Default
默認調度器腌巾。它使用JVM的共享線程池,該調度器的最大并發(fā)度是CPU的核心數固阁,默認為2
Dispatcher.Unconfined
非受限調度器壤躲,它不會將操作限制在任何線程上執(zhí)行——在發(fā)起協程的線程上執(zhí)行第一個掛起點之前的操作,在掛起點恢復后由對應的掛起函數決定接下來在哪個線程上執(zhí)行备燃。
Dispathcer.IO
IO調度器碉克,他將阻塞的IO任務分流到一個共享的線程池中,使得不阻塞當前線程并齐。該線程池大小為環(huán)境變量kotlinx.coroutines.io.parallelism的值漏麦,默認是64或核心數的較大者客税。
該調度器和Dispatchers.Default共享線程,因此使用withContext(Dispatchers.IO)創(chuàng)建新的協程不一定會導致線程的切換撕贞。
Dispathcer.Main
該調度器限制所有執(zhí)行都在UI主線程更耻,它是專門用于UI的,并且會隨著平臺的不同而不同
對于JS或Native捏膨,其效果等同于Dispatchers.Default
對于JVM秧均,它是Android的主線程、JavaFx或者Swing EDT的dispatcher之一号涯。
并且為了使用該調度器目胡,還必須增加對應的組件
kotlinx-coroutines-android
kotlinx-coroutines-javafx
kotlinx-coroutines-swing
其它
在其它支持協程的第三方庫中,也存在對應的調度器链快,如Vertx的vertx.dispatcher()誉己,它將協程分配到vertx的EventLoop線程池執(zhí)行。
注意域蜗,由于上下文具有繼承關系巨双,因此啟動子協程時不顯式指定調度器時,子協程和父協程是使用相同調度器的霉祸。
Job
Job也是上下文元素筑累,它代表協程本身。Job能夠被組織成父子層次結構脉执,并具有如下重要特性疼阔。
父Job退出,所有子job會馬上退出
子job拋出除CancellationException(意味著正常取消)意外的異常會導致父Job馬上退出
類似Thread半夷,一個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 |
作用域
協程作用域——CoroutineScope婆廊,用于管理協程,管理的內容有
- 啟動協程的方式 - 它定義了launch巫橄、async淘邻、withContext等協程啟動方法(以extention的方式),并在這些方法內定義了啟動子協程時上下文的繼承方式湘换。
- 管理協程生命周期 - 它定義了cancel()方法宾舅,用于取消當前作用域,同時取消作用域內所有協程彩倚。
fun test(){
viewModelScope.launch(Dispatchers.Main) {
print("1:" + Thread.currentThread().name)
withContext(Dispatchers.IO){
delay(1000)
print("2:" + Thread.currentThread().name)
}
print("3:" + Thread.currentThread().name)
}
}
//1,2帆离,3處分別輸出main,DefaultDispatcher-worker-1,main
區(qū)分作用域和上下文
從類定義看蔬蕊,CoroutineScope和CoroutineContext非常類似哥谷,最終目的都是協程上下文岸夯,但正如Kotlin協程負責人Roman Elizarov在Coroutine Context and Scope中所說麻献,二者的區(qū)別只在于使用目的的不同——作用域用于管理協程;而上下文只是一個記錄協程運行環(huán)境的集合煮盼。
Flow
我的理解跟rxjava 差不多,感興趣可以看官方文檔。