協(xié)程在Kotlin中文文檔的解釋是輕量級(jí)的線程纯路,Go姥份、Python 等很多現(xiàn)成語(yǔ)言在語(yǔ)言層面上都實(shí)現(xiàn)協(xié)程凡伊,不過(guò)Kotlin和他們不同的的是可帽,Kotlin協(xié)程本質(zhì)上只是一套基于原生Java線程池 的封裝钾军,Kotlin 協(xié)程的核心競(jìng)爭(zhēng)力在于:它能簡(jiǎn)化異步并發(fā)任務(wù),以同步方式寫(xiě)異步代碼鳄袍。
- 掛起:suspend
在協(xié)程里suspend是一個(gè)重要的關(guān)鍵字,這個(gè)關(guān)鍵字只是起到的提醒的作用吏恭,當(dāng)代碼執(zhí)行到suspend時(shí)拗小,會(huì)從當(dāng)前線程掛起這個(gè)函數(shù),然后代碼繼續(xù)執(zhí)行樱哼,而掛起的函數(shù)從當(dāng)前線程脫離哀九,然后繼續(xù)執(zhí)行,這個(gè)時(shí)候在哪個(gè)線程執(zhí)行搅幅,由協(xié)程調(diào)度器所指定阅束,掛起函數(shù)執(zhí)行完之后,又會(huì)重新切回到它原先的線程來(lái)茄唐。這個(gè)就是協(xié)程的優(yōu)勢(shì)所在息裸。
private fun test3(){
Log.e("test", "start")
lifecycleScope.launch {
launch()
}
Log.e("test", "end")
}
suspend fun launch(){
Log.e("test", "launch_start")
delay(3000)
Log.e("test", "launch_end")
}
運(yùn)行結(jié)果如下:
從截圖可以看出,launch函數(shù)被掛起沪编,然后主要流程繼續(xù)執(zhí)行界牡,而launch函數(shù)被掛起后也繼續(xù)執(zhí)行。
- 掛起函數(shù)線程切換
從上面看我們已經(jīng)掛起了函數(shù)漾抬,讓程序脫離當(dāng)前的線程,kotlin 協(xié)程提供了一個(gè) withContext() 方法常遂,來(lái)實(shí)現(xiàn)線程切換纳令。在講切線程之前,我們先說(shuō)說(shuō)Dispatchers調(diào)度器克胳,它可以將協(xié)程限制在一個(gè)特定的線程執(zhí)行平绩,或者將它分派到一個(gè)線程池,或者讓它不受限制地運(yùn)行漠另。
Dispatchers調(diào)度器種類
- Dispatchers.Main:Android 中的主線程
- Dispatchers.IO:針對(duì)磁盤(pán)和網(wǎng)絡(luò) IO 進(jìn)行了優(yōu)化捏雌,適合 IO 密集型的任務(wù),比如:讀寫(xiě)文件笆搓,操作數(shù)據(jù)庫(kù)以及網(wǎng)絡(luò)請(qǐng)求
- Dispatchers.Default:適合 CPU 密集型的任務(wù)性湿,比如計(jì)算
- Dispatchers.Unconfined:當(dāng)我們不關(guān)心協(xié)程在哪個(gè)線程上被掛起時(shí)使用
那我們?cè)趺辞袚Q線程呢纬傲,
suspend fun launch() {
withContext(Dispatchers.IO){
Log.e("test2", Thread.currentThread().name)
delay(3000)
Log.e("test", "launch_end")
}
}
在lifecycleScope里,就更簡(jiǎn)單了肤频,直接如下
private fun test3() {
Log.e("test", "start")
lifecycleScope.launch(Dispatchers.IO) {
Log.e("test", "launch_start")
delay(3000)
withContext(Dispatchers.Main){
Log.e("test", "launch_end")
}
}
Log.e("test", "end")
}
在協(xié)程里叹括,線程切換就是這么簡(jiǎn)單,在io線程執(zhí)行耗時(shí)任務(wù)宵荒,然后又在main里切會(huì)主線程汁雷。
協(xié)程的取消,追蹤協(xié)程的狀態(tài)
我們開(kāi)啟了一個(gè)協(xié)程报咳,在協(xié)程執(zhí)行期間也想操作這個(gè)協(xié)程侠讯,這就是要用到Job,什么是Job,從概念上講,一個(gè) Job 表示具有生命周期的暑刃、可以取消的東西厢漩。從形態(tài)上將,Job 是一個(gè)接口稍走,但是它有具體的合約和狀態(tài)袁翁,所以它可以被當(dāng)做一個(gè)抽象類來(lái)看待。
Job 一共包含六個(gè)狀態(tài):
- 新創(chuàng)建 New
- 活躍 Active
- 完成中 Completing
- 已完成 Completed
- 取消中 Cancelling
- 已取消 Cancelled
Job 的生命周期會(huì)經(jīng)過(guò)四個(gè)狀態(tài):New → Active → Completing → Completed婿脸。
下面來(lái)講講job的常用方法:
- join() 掛起協(xié)程粱胜,直到任務(wù)完成再恢復(fù)
private suspend fun test() {
Log.e("test", "start")
job=lifecycleScope.launch(Dispatchers.IO) {
launch()
}
job?.join()
Log.e("test", "end")
}
private suspend fun launch() {
withContext(Dispatchers.IO){
Log.e("test2", "launch_start")
delay(3000)
Log.e("test", "launch_end")
}
}
運(yùn)行結(jié)果如下:
可以看到,加上join狐树,協(xié)程代碼會(huì)在這里執(zhí)行焙压,并且是阻塞的,只有執(zhí)行完才會(huì)走下一步
cancel() 取消協(xié)程
cancelAndJoin() 取消并掛起調(diào)用協(xié)程抑钟,直到被取消的協(xié)程完成
private suspend fun test() {
Log.e("test", "start")
job=lifecycleScope.launch(Dispatchers.IO) {
launch()
}
job?.cancelAndJoin()
Log.e("test", "end")
}
運(yùn)行結(jié)果如下:
可以看待涯曲,cancelAndJoin(),會(huì)運(yùn)行,然后取消在塔,取消完后會(huì)走下一步
- start() 如果Job所在的協(xié)程還沒(méi)有被啟動(dòng)那么調(diào)用這個(gè)方法就會(huì)啟動(dòng)協(xié)程,如果這個(gè)協(xié)程被啟動(dòng)了返回true幻件,如果已經(jīng)啟動(dòng)或者執(zhí)行完畢了返回false
代碼如下:
private suspend fun test3() {
Log.e("test", "start")
job=lifecycleScope.launch(start = CoroutineStart.LAZY) {
launch()
}
Log.e("test", "end")
job?.start()
}
運(yùn)行效果如下:
可以看到當(dāng)設(shè)置延遲加載時(shí),協(xié)程是start()后才開(kāi)始執(zhí)行
說(shuō)到延遲加載蛔溃,在總結(jié)一下協(xié)程啟動(dòng)模式
DEFAULT 模式
默認(rèn)的 協(xié)程啟動(dòng)模式 , 協(xié)程創(chuàng)建后 , 馬上開(kāi)始調(diào)度執(zhí)行 , 如果在 執(zhí)行前或執(zhí)行時(shí) 取消協(xié)程 , 則進(jìn)入 取消響應(yīng) 狀態(tài) ; 如果在執(zhí)行過(guò)程中取消 , 協(xié)程也會(huì)被取消 ;ATOMIC 模式
協(xié)程創(chuàng)建后 , 馬上開(kāi)始調(diào)度執(zhí)行 , 協(xié)程執(zhí)行到 第一個(gè)掛起點(diǎn) 之前 , 如果取消協(xié)程 , 則不進(jìn)行響應(yīng)取消操作 ;LAZY 模式
協(xié)程創(chuàng)建后 , 不會(huì)馬上開(kāi)始調(diào)度執(zhí)行 , 只有 主動(dòng)調(diào)用協(xié)程的 start , join , await 方法 時(shí) , 才開(kāi)始調(diào)度執(zhí)行協(xié)程 , 如果在 調(diào)度之前取消協(xié)程 , 該協(xié)程直接報(bào)異常 進(jìn)入異常響應(yīng)狀態(tài) ;-
UNDISPATCHED 模式
協(xié)程創(chuàng)建后,立即在當(dāng)前的函數(shù)調(diào)用棧執(zhí)行協(xié)程任務(wù),直到遇到第一個(gè)掛起函數(shù),才在子線程中執(zhí)行掛起函數(shù) ;- 如果在主線程中啟動(dòng)協(xié)程 , 則該模式的協(xié)程就會(huì)直接在主線程中執(zhí)行 ;
- 如果在子線程中啟動(dòng)協(xié)程 , 則該模式的協(xié)程就會(huì)直接在子線程中執(zhí)行 ;
協(xié)程異常處理
對(duì)于不同協(xié)程構(gòu)造器绰沥,異常的處理方式不同。分別介紹 launch 和 async 情況下的異常處理
- Launch
launch 方式啟動(dòng)的協(xié)程贺待,異常會(huì)在發(fā)生時(shí)立刻拋出徽曲,使用 try catch 就可以將協(xié)程中的異常捕獲。
scope.launch {
try {
codeThatCanThrowExceptions()
} catch(e: Exception) {
// Handle exception
}finally{
//結(jié)束處理
}
}
也可以try catch整個(gè)協(xié)程
try {
scope.launch {
codeThatCanThrowExceptions()
}
} catch(e: Exception) {
// Handle exception
}finally{
//結(jié)束處理
}
}
- Async
當(dāng) async 開(kāi)啟的協(xié)程為根協(xié)程 或 supervisorScope 的直接子協(xié)程時(shí)麸塞,異常在調(diào)用 await 時(shí)拋出秃臣,使用 try catch 可以捕獲異常:
fun main() = runBlocking {
val deferred = GlobalScope.async {
throw Exception()
}
try {
deferred.await() //拋出異常
} catch (t: Throwable) {
println("捕獲異常:$t")
}finally{
//結(jié)束處理
}
}
協(xié)程并行
到目前為止,上面的代碼都是串行的哪工,即從上到下依次執(zhí)行奥此,而協(xié)程不單單串行弧哎,我們也可以并行的方式。
- 使用 async 并發(fā)
private fun test() {
Log.e("test", "start")
lifecycleScope.async {
Log.e("test", "launch1_start")
delay(1000)
Log.e("test", "launch1_end")
}
lifecycleScope.async {
Log.e("test", "launch2_start")
delay(2000)
Log.e("test", "launch2_end")
}
Log.e("test", "end")
}
運(yùn)行效果如下
可以看到兩個(gè)協(xié)程可以并行執(zhí)行得院,也可以用await()方法傻铣,代碼如下:
private fun test4(){
lifecycleScope.launch {
val a=async {
delay(1000)
1
}
val b=async {
delay(5000)
2
}
var c=a.await()+b.await()
Log.e("test",c.toString())
}
}
通過(guò)await()方法,即使兩個(gè)協(xié)程完成時(shí)間不一致祥绞,最終也可以一起運(yùn)算非洲。
協(xié)程-并發(fā)處理
從上面可以了解到,協(xié)程也是可以并發(fā)的蜕径,既然是并發(fā)两踏,那同樣也會(huì)出現(xiàn)像java多線程并發(fā)的問(wèn)題,導(dǎo)致各種問(wèn)題兜喻,協(xié)程本身也提供了兩種方式處理并發(fā):
- Mutex
Mutex 類似于 synchorinzed梦染,協(xié)程競(jìng)爭(zhēng)時(shí)將協(xié)程包裝為 LockWaiter 使用雙向鏈表存儲(chǔ)。Mutex通俗點(diǎn)來(lái)說(shuō)就是kotlin的鎖朴皆,和java 的synchronized和RecentLock對(duì)應(yīng)帕识。
使用mutex.withLock {*} 即可實(shí)現(xiàn)數(shù)據(jù)的同步以簡(jiǎn)化使用,下面給個(gè)事例:
在沒(méi)有加鎖之前:
private fun test(){
repeat(5) {
GlobalScope.launch(Dispatchers.IO) {
delay(2000)
value++
Log.e("test",value.toString())
}
}
}
開(kāi)啟五個(gè)協(xié)程遂铡,同時(shí)運(yùn)行肮疗,對(duì)value操作:
可以看到,順序是亂的扒接,而加了mutex之后呢:
var mutex= Mutex()
private fun test(){
repeat(5) {
GlobalScope.launch(Dispatchers.IO) {
delay(2000)
mutex.withLock {
value++
Log.e("test",value.toString())
}
}
}
}
運(yùn)行結(jié)果如下:
可以看到伪货,加鎖之后,都會(huì)等上一個(gè)運(yùn)行后之后在解鎖钾怔,在運(yùn)行下一個(gè)碱呼。
- Actors
一個(gè) actor 是由協(xié)程、被限制并封裝到該協(xié)程中的狀態(tài)以及一個(gè)與其它協(xié)程通信的 通道 組合而成的一個(gè)實(shí)體宗侦。一個(gè)簡(jiǎn)單的actor 可以簡(jiǎn)單的寫(xiě)成一個(gè)函數(shù),但是一個(gè)擁有復(fù)雜狀態(tài)的actor更適合由類來(lái)表示愚臀。
有一個(gè) actor 協(xié)程構(gòu)建器,它可以方便地將 actor 的通道組合到其作用域中(用來(lái)接收消息)矾利、組合發(fā)送 channel 與結(jié)果集對(duì)象懊悯,這樣對(duì)actor的單個(gè)引用就可以作為其句柄持有。
使用 actor 的第一步是定義一個(gè) actor 要處理的消息類梦皮。
// 計(jì)數(shù)器 Actor 的各種類型
sealed class CounterMsg
object IncCounter : CounterMsg() // 遞增計(jì)數(shù)器的單向消息
class GetCounter(val response: CompletableDeferred<Int>) : CounterMsg() // 攜帶回復(fù)的請(qǐng)求
接下來(lái)定義一個(gè)函數(shù),使用 actor 協(xié)程構(gòu)建器來(lái)啟動(dòng)一個(gè) actor:
// 這個(gè)函數(shù)啟動(dòng)一個(gè)新的計(jì)數(shù)器 actor
fun CoroutineScope.counterActor() = actor<CounterMsg> {
var counter = 0 // actor 狀態(tài)
for (msg in channel) { // 即將到來(lái)消息的迭代器
when (msg) {
is IncCounter -> counter++
is GetCounter -> msg.response.complete(counter)
}
}
}
執(zhí)行代碼:
runBlocking {
val counterActor = counterActor() // 創(chuàng)建該 actor
repeat(100) {
launch {
repeat(1000) {
counterActor.send(IncCounter)
}
}
}
delay(3000)
// 發(fā)送一條消息以用來(lái)從一個(gè) actor 中獲取計(jì)數(shù)值
val response = CompletableDeferred<Int>()
counterActor.send(GetCounter(response))
println("Counter = ${response.await()}")
counterActor.close() // 關(guān)閉該actor
}
actor 可以修改自己的私有狀態(tài)桃焕,但只能通過(guò)消息互相影響(避免任何鎖定)剑肯。actor 在高負(fù)載下比鎖更有效,因?yàn)樵谶@種情況下它總是有工作要做观堂,而且根本不需要切換到不同的上下文让网,這樣效率更高呀忧。
協(xié)程的創(chuàng)建
寫(xiě)到這里,基本上把協(xié)程的基本用法都說(shuō)了溃睹,最后要用協(xié)程而账,要知道這么創(chuàng)建協(xié)程吧,其實(shí)這里也有分的因篇,所以才放在最后泞辐,假如是單單在kotlin里創(chuàng)建協(xié)程,就有三種方式
- 使用 runBlocking 頂層函數(shù)創(chuàng)建:
runBlocking {
...
}
通常適用于單元測(cè)試的場(chǎng)景竞滓,而業(yè)務(wù)開(kāi)發(fā)中不會(huì)用到這種方法咐吼,因?yàn)樗蔷€程阻塞的。
- 使用 GlobalScope 單例對(duì)象創(chuàng)建:
GlobalScope.launch {
...
}
GlobalScope和使用 runBlocking 的區(qū)別在于不會(huì)阻塞線程商佑。但在 Android 開(kāi)發(fā)中同樣不推薦這種用法,因?yàn)樗纳芷跁?huì)只受整個(gè)應(yīng)用程序的生命周期限制,且不能取消郊闯。
- 自行通過(guò) CoroutineContext 創(chuàng)建一個(gè) CoroutineScope 對(duì)象:
val coroutineScope = CoroutineScope(context)
coroutineScope.launch {
...
}
這是比較推薦的使用方法舌涨,我們可以通過(guò) context 參數(shù)去管理和控制協(xié)程的生命周期(這里的 context 和 Android 里的不是一個(gè)東西,是一個(gè)更通用的概念抓半,會(huì)有一個(gè) Android 平臺(tái)的封裝來(lái)配合使用)喂急。
Android平臺(tái)協(xié)程創(chuàng)建
首先需要引用ktx庫(kù)
implementation "androidx.lifecycle:lifecycle-runtime-ktx:版本號(hào)"
這個(gè)時(shí)候我們就可以在activity或者framgent直接使用lifecycleScope進(jìn)行啟動(dòng)協(xié)程。就像我上面的代碼實(shí)例一樣琅关。
lifecycleScope和lifecycle的生命周期一致煮岁,退出的時(shí)候也可以自動(dòng)取消協(xié)程,不用自己手動(dòng)取消涣易。
同時(shí)画机,還擴(kuò)展了lifecycleScope.launchWhenResumed , lifecycleScope.launchWhenCreated 新症,lifecycleScope.launchWhenStarted步氏,
分別對(duì)應(yīng)activity或者fragment的onResumed(),onCreated(),onStarted().
- viewLifecycleOwner
雖然fragment也可以用lifecycleScope,但是最好還是viewLifecycleOwner徒爹,因?yàn)镕ragment與Fragment中的View的生命周期并不一致荚醒,需要讓observer感知Fragment中的View的生命周期而非Fragment,
ViewModel中使用協(xié)程
同樣引入擴(kuò)展庫(kù)
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:版本號(hào)"
引入庫(kù)之后隆嗅,我們就可以在ViewModel用viewModelScope來(lái)使用協(xié)程.
其他環(huán)境下使用協(xié)程
其他情況下的創(chuàng)建按照上面協(xié)程推薦的第三種方式即可