前言
公司開啟新項(xiàng)目了,想著準(zhǔn)備亮一手 Kotlin 協(xié)程應(yīng)用到項(xiàng)目中去,之前有對 Kotlin 協(xié)程的知識進(jìn)行一定量的學(xué)習(xí),以為自己理解協(xié)程了,結(jié)果……實(shí)在拿不出手!
為了更好的加深記憶和理解,更全面系統(tǒng)深入地學(xué)習(xí) Kotlin 協(xié)程的知識,協(xié)程將分為三部分來講解,本文是第一篇
一、概述
協(xié)程的概念在1958年就開始出現(xiàn)(比線程還早), 目前很多語言開始原生支, Java 沒有原生協(xié)程但是大型公司都自己或者使用第三方庫來支持協(xié)程編程, 但是Kotlin原生支持協(xié)程。
Android 中的每個(gè)應(yīng)用都會運(yùn)行一個(gè)主線程,它主要是用來處理 UI,如果主線程上需要處理的任務(wù)太多,應(yīng)用就感覺被卡主一樣影響用戶體驗(yàn),得讓那些耗時(shí)的任務(wù)不阻塞主線程的運(yùn)行。要做到處理網(wǎng)絡(luò)請求不會阻塞主線程,一個(gè)常用的做法就是使用回調(diào),另一種是使用協(xié)程。
協(xié)程概念
很多人都會問協(xié)程是什么?這里引用官方的解釋:
1.協(xié)程通過將復(fù)雜性放入庫來簡化異步編程。程序的邏輯可以在協(xié)程中順序地表達(dá),而底層庫會為我們解決其異步性。該庫可以將用戶代碼的相關(guān)部分包裝為回調(diào)、訂閱相關(guān)事件、在不同線程(甚至不同機(jī)器)上調(diào)度執(zhí)行,而代碼則保持如同順序執(zhí)行一樣簡單。
2.協(xié)程是一種并發(fā)設(shè)計(jì)模式。
協(xié)程就像輕量級的線程,為什么是輕量的?因?yàn)閰f(xié)程是依賴于線程,一個(gè)線程中可以創(chuàng)建N個(gè)協(xié)程, 很重要的一點(diǎn)就是協(xié)程掛起時(shí)不會阻塞線程 ,幾乎是無代價(jià)的。而且它 基于線程池API ,所以在處理并發(fā)任務(wù)這件事上它真的游刃有余。
協(xié)程只是一種概念,它提供了一種避免阻塞線程并用更簡單、更可控的操作替代線程阻塞的方法: 協(xié)程掛起和恢復(fù) 。 本質(zhì)上Kotlin協(xié)程就是作為在Kotlin語言上進(jìn)行異步編程的解決方案,處理異步代碼的方法 。
有可能有的同學(xué)問了,既然它基于線程池,那我直接使用線程池或者使用 Android 中其他的異步任務(wù)解決方案,比如 Handler、AsyncTask、RxJava等,不更好嗎?
協(xié)程可以 使用阻塞的方式寫出非阻塞式的代碼 ,解決并發(fā)中常見的回調(diào)地獄。消除了并發(fā)任務(wù)之間的協(xié)作的難度,協(xié)程可以讓我們輕松地寫出復(fù)雜的并發(fā)代碼。一些本來不可能實(shí)現(xiàn)的并發(fā)任務(wù)變的可能,甚至簡單,這些才是協(xié)程的優(yōu)勢所在。
作用
- 1.協(xié)程可以讓異步代碼同步化 ;
- 2.協(xié)程可以降低異步程序的設(shè)計(jì)復(fù)雜度 。
特點(diǎn)
- 輕量 :您可以在單個(gè)線程上運(yùn)行多個(gè)協(xié)程,因?yàn)閰f(xié)程支持掛起,不會使正在運(yùn)行協(xié)程的線程阻塞。掛起比阻塞節(jié)省內(nèi)存,且支持多個(gè)并行操作。
- 內(nèi)存泄漏更少 :使用結(jié)構(gòu)化并發(fā)機(jī)制在一個(gè)作用域內(nèi)執(zhí)行多項(xiàng)操作。
- 內(nèi)置取消支持 :取消操作會自動(dòng)在運(yùn)行中的整個(gè)協(xié)程層次結(jié)構(gòu)內(nèi)傳播。
- Jetpack 集成 :許多 Jetpack 庫都包含提供全面協(xié)程支持的擴(kuò)展。某些庫還提供自己的協(xié)程作用域,可供您用于結(jié)構(gòu)化并發(fā)。
Kotlin Coroutine 生態(tài)
kotlin的協(xié)程實(shí)現(xiàn)分為了兩個(gè)層次:
- 基礎(chǔ)設(shè)施層 :標(biāo)準(zhǔn)庫的協(xié)程API,主要對協(xié)程提供了概念和語義上最基本的支持;
- 業(yè)務(wù)框架層 kotlin.coroutines :協(xié)程的上層框架支持,基于標(biāo)準(zhǔn)庫實(shí)現(xiàn)的封裝,也是我們?nèi)粘i_發(fā)使用的協(xié)程擴(kuò)展庫。
依賴庫
在 project
的 gradle
添加 Kotlin
編譯插件:
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.32"
}
要使用協(xié)程,還需要在app的 build.gradle
文件中添加依賴:
dependencies {
//協(xié)程標(biāo)準(zhǔn)庫
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
//協(xié)程核心庫
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
//協(xié)程Android支持庫,提供安卓UI調(diào)度器
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
}
這里我們主要使用協(xié)程擴(kuò)展庫, kotlin協(xié)程標(biāo)準(zhǔn)庫太過于簡陋不適用于開發(fā)者使用。
二、原理
協(xié)程的概念最核心的點(diǎn)就是函數(shù)或者一段程序能夠被掛起,稍后再在掛起的位置恢復(fù) 。協(xié)程通過主動(dòng)讓出運(yùn)行權(quán)來實(shí)現(xiàn)協(xié)作,程序自己處理掛起和恢復(fù)來實(shí)現(xiàn)程序執(zhí)行流程的協(xié)作調(diào)度。因此它本質(zhì)上就是在討論程序控制流程的機(jī)制。
使用場景
kotlin協(xié)程基于Thread相關(guān)API的封裝,讓我們不用過多關(guān)心線程也可以方便地寫出并發(fā)操作,這就是Kotlin的協(xié)程。協(xié)程的好處本質(zhì)上和其他線程api一樣, 方便 。
在 Android 平臺上,協(xié)程有兩個(gè)主要使用場景:
- 1、線程切換,保證線程安全。
- 2、處理耗時(shí)任務(wù)(比如網(wǎng)絡(luò)請求、解析
JSON
數(shù)據(jù)、從數(shù)據(jù)庫中進(jìn)行讀寫操作等)。
Kotlin協(xié)程的原理
我們使用 Retrofit
發(fā)起了一個(gè)異步請求,從服務(wù)端查詢用戶的信息,通過 CallBack
返回 response
:
val call: Call
很明顯我們需要處理很多的回調(diào)分支,如果業(yè)務(wù)多則更容易陷入「回調(diào)地獄」繁瑣凌亂的代碼中。
使用協(xié)程,同樣可以像 Rx 那樣有效地消除回調(diào)地獄,不過無論是設(shè)計(jì)理念,還是代碼風(fēng)格,兩者是有很大區(qū)別的,協(xié)程在寫法上和普通的順序代碼類似,同步的方式去編寫異步執(zhí)行的代碼。使用協(xié)程改造后代碼如下:
GlobalScope.launch(Dispatchers.Main) {//開始協(xié)程:主線程
val result = userApi.getUserSuspend("suming")//網(wǎng)絡(luò)請求(IO 線程)
tv_name.text = result?.name //更新 UI(主線程)
}
這就是kotlin最有名的【非阻塞式掛起】,使用同步的方式完成異步任務(wù),而且很簡潔,這是Kotlin協(xié)程的魅力所在。之所有可以用看起來同步的方式寫異步代碼,關(guān)鍵在于請求函數(shù)getUserSuspend()
是一個(gè) 掛起函數(shù) ,被suspend
關(guān)鍵字修飾,下面會介紹。
在上面的協(xié)程的原理圖解中,耗時(shí)阻塞的操作并沒有減少,只是交給了其他線程。userApi.getUserSuspend("suming")
真正執(zhí)行的時(shí)候會切換到IO線程中執(zhí)行,獲取結(jié)果后最后恢復(fù)到主線程上,然后繼續(xù)執(zhí)行剩下的流程。
將業(yè)務(wù)流程原理拆分得更細(xì)致一點(diǎn),在主線程中創(chuàng)建協(xié)程A
中執(zhí)行整個(gè)業(yè)務(wù)流程,如果遇到異步調(diào)用任務(wù)則協(xié)程A
被掛起,切換到IO線程中創(chuàng)建子協(xié)程B
,獲取結(jié)果后再恢復(fù)到主線程的協(xié)程A
上,然后繼續(xù)執(zhí)行剩下的流程。
協(xié)程Coroutine雖然不能脫離線程而運(yùn)行,但可以在不同的線程之間切換,而且一個(gè)線程上可以一個(gè)或多個(gè)協(xié)程。下圖動(dòng)態(tài)顯示了進(jìn)程 - 線程 - 協(xié)程微妙關(guān)系。
此動(dòng)圖來源
三、基礎(chǔ)
GlobalScope.launch(Dispatchers.Main) {//開始協(xié)程:主線程
val result = userApi.getUserSuspend("suming")//網(wǎng)絡(luò)請求(IO 線程)
tv_name.text = result?.name //更新 UI(主線程)
}
上面就是啟動(dòng)協(xié)程的代碼,啟動(dòng)協(xié)程的代碼可以分為三部分:GlobalScope
、launch
、Dispatchers
,它們分別對應(yīng):協(xié)程的作用域、構(gòu)建器和調(diào)度器。
1.協(xié)程的構(gòu)建
上面的GlobalScope.launch()
屬于協(xié)程構(gòu)建器Coroutine builders
,Kotlin 中還有其他幾種 Builders, 負(fù)責(zé)創(chuàng)建協(xié)程 :
runBlocking:T
:頂層函數(shù),創(chuàng)建一個(gè)新的協(xié)程同時(shí)阻塞當(dāng)前線程,直到其內(nèi)部所有邏輯以及子協(xié)程所有邏輯全部執(zhí)行完成,返回值是泛型T
,一般在項(xiàng)目中不會使用,主要是為main函數(shù)和測試設(shè)計(jì)的。launch
:?創(chuàng)建一個(gè)新的協(xié)程,不會阻塞當(dāng)前線程,必須在協(xié)程作用域中才可以調(diào)用。它返回的是一個(gè)該協(xié)程任務(wù)的引用,即Job
對象。這是最常用的用于啟動(dòng)協(xié)程的方式。async
:?創(chuàng)建一個(gè)新的協(xié)程,不會阻塞當(dāng)前線程,必須在協(xié)程作用域中才可以調(diào)用。并返回Deffer
對象,可通過調(diào)用Deffer.await()
方法等待該子協(xié)程執(zhí)行完成并獲取結(jié)果。常用于并發(fā)執(zhí)行-同步等待和獲取返回值的情況。
runBlocking
fun runBlocking(context: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.() -> T): T
- context:??協(xié)程的上下文,表示協(xié)程的運(yùn)行環(huán)境,包括協(xié)程調(diào)度器、代表協(xié)程本身的Job、協(xié)程名稱、協(xié)程ID等,默認(rèn)值是當(dāng)前線程上的事件循環(huán)。(這里的
context
和Android的context
不同,后面會講解到) - block:???協(xié)程執(zhí)行體,是一個(gè)用suspend關(guān)鍵字修飾的一個(gè)無參,無返回值的函數(shù)類型。是一個(gè)帶接收者的函數(shù)字面量,接收者是 CoroutineScope ,因此執(zhí)行體包含了一個(gè)隱式的
CoroutineScope
,所以在runBlocking
內(nèi)部可以來直接啟動(dòng)協(xié)程。 - T:?????返回值是泛型
T
,協(xié)程體block
中最后一行返回的是什么類型T
就是什么類型。
它是一個(gè)頂層函數(shù),不是GlobalScope
的 API,可以在任意地方獨(dú)立使用。它能創(chuàng)建一個(gè)新的協(xié)程同時(shí)阻塞當(dāng)前線程,直到其內(nèi)部所有邏輯以及子協(xié)程所有邏輯全部執(zhí)行完成,它的目的是將常規(guī)的阻塞代碼與以掛起suspend
風(fēng)格編寫的庫連接起來,常用于main
函數(shù)和測試中。一般我們在項(xiàng)目中是不會使用的。
fun runBloTest() {
print("start")
//context上下文使用默認(rèn)值,阻塞當(dāng)前線程,直到代碼塊中的邏輯完成
runBlocking {
//這里是協(xié)程體
delay(1000)//掛起函數(shù),延遲1000毫秒
print("runBlocking")
}
print("end")
}
打印數(shù)據(jù)如下:
runBlocking.gif
只有在runBlocking
協(xié)程體邏輯全部運(yùn)行結(jié)束后,聲明在runBlocking
之后的代碼才能執(zhí)行,即runBlocking
會阻塞其所在線程。
注意:runBlocking
雖然會阻塞當(dāng)前線程的,但其內(nèi)部運(yùn)行的協(xié)程又是非阻塞的。
launch
launch
是最常用的用于啟動(dòng)協(xié)程的方式,用于在不阻塞當(dāng)前線程的情況下啟動(dòng)一個(gè)協(xié)程,并返回對該協(xié)程任務(wù)的引用,即Job
對象。
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job
- context:?協(xié)程的上下文,表示協(xié)程的運(yùn)行環(huán)境,包括協(xié)程調(diào)度器、代表協(xié)程本身的Job、協(xié)程名稱、協(xié)程ID等,默認(rèn)值是當(dāng)前線程上的事件循環(huán)。
- start: ?協(xié)程啟動(dòng)模式,這些啟動(dòng)模式的設(shè)計(jì)主要是為了應(yīng)對某些特殊的場景。業(yè)務(wù)開發(fā)實(shí)踐中通常使用DEFAULT和LAZY這兩個(gè)啟動(dòng)模式就夠了。
- block:??協(xié)程代碼,它將在提供的范圍的上下文中被調(diào)用。它是一個(gè)用
suspend
(掛起函數(shù))關(guān)鍵字修飾的一個(gè)無參,無返回值的函數(shù)類型。接收者是CoroutineScope
的函數(shù)字面量。 - Job:???協(xié)程構(gòu)建函數(shù)的返回值,可以把
Job
看成協(xié)程對象本身,封裝了協(xié)程中需要執(zhí)行的代碼邏輯,是協(xié)程的唯一標(biāo)識,Job可以取消,并且負(fù)責(zé)管理協(xié)程的生命周期。
協(xié)程需要運(yùn)行在協(xié)程上下文環(huán)境中 (即協(xié)程作用域,下面會講解到),在非協(xié)程環(huán)境中launch
有兩種方式創(chuàng)建協(xié)程:
GlobalScope.launch()
在應(yīng)用范圍內(nèi)啟動(dòng)一個(gè)新協(xié)程,不會阻塞調(diào)用線程,協(xié)程的生命周期與應(yīng)用程序一致。表示一個(gè)不綁定任何Job
的全局作用域,用于啟動(dòng)頂層協(xié)程,這些協(xié)程在整個(gè)應(yīng)用程序生命周期中運(yùn)行,不會提前取消(不存在Job
)。
fun launchTest() {
print("start")
//創(chuàng)建一個(gè)全局作用域協(xié)程,不會阻塞當(dāng)前線程,生命周期與應(yīng)用程序一致
GlobalScope.launch {
//在這1000毫秒內(nèi)該協(xié)程所處的線程不會阻塞
//協(xié)程將線程的執(zhí)行權(quán)交出去,該線程繼續(xù)干它要干的事情,到時(shí)間后會恢復(fù)至此繼續(xù)向下執(zhí)行
delay(1000)//1秒無阻塞延遲(默認(rèn)單位為毫秒)
print("GlobalScope.launch")
}
print("end")//主線程繼續(xù),而協(xié)程被延遲
}
GlobalScope.launch()
協(xié)程將線程的執(zhí)行權(quán)交出去,該線程繼續(xù)干它要干的事情,主線程繼續(xù),而協(xié)程被延遲,到時(shí)間后會恢復(fù)至此繼續(xù)向下執(zhí)行。
打印數(shù)據(jù)如下:
launch1.gif
由于這樣啟動(dòng)的協(xié)程存在組件已被銷毀但協(xié)程還存在的情況,極限情況下可能導(dǎo)致資源耗盡,尤其是在 Android 客戶端這種需要頻繁創(chuàng)建銷毀組件的場景,因此不推薦這種用法。
注意:這里說的是GlobalScope
沒有Job
, 但是啟動(dòng)的launch
是有Job
的。 GlobalScope
本身就是一個(gè)作用域, launch
屬于其子作用域。
CoroutineScope.launch()
啟動(dòng)一個(gè)新的協(xié)程而不阻塞當(dāng)前線程,并返回對協(xié)程的引用作為一個(gè)Job
。通過CoroutineContext
至少一個(gè)協(xié)程上下文參數(shù)創(chuàng)建一個(gè) CoroutineScope
對象。協(xié)程上下文控制協(xié)程生命周期和線程調(diào)度,使得協(xié)程和該組件生命周期綁定,組件銷毀時(shí),協(xié)程一并銷毀,從而實(shí)現(xiàn)安全可靠地協(xié)程調(diào)用。這是在應(yīng)用中最推薦使用的協(xié)程使用方式。
fun launchTest2() {
print("start")
//開啟一個(gè)IO模式的協(xié)程,通過協(xié)程上下文創(chuàng)建一個(gè)CoroutineScope對象,需要一個(gè)類型為CoroutineContext的參數(shù)
val job = CoroutineScope(Dispatchers.IO).launch {
delay(1000)//1秒無阻塞延遲(默認(rèn)單位為毫秒)
print("CoroutineScope.launch")
}
print("end")//主線程繼續(xù),而協(xié)程被延遲
}
-
Android
+關(guān)注
關(guān)注
12文章
3959瀏覽量
129295 -
JAVA
+關(guān)注
關(guān)注
20文章
2983瀏覽量
106598 -
ui
+關(guān)注
關(guān)注
0文章
206瀏覽量
21638 -
kotlin
+關(guān)注
關(guān)注
0文章
60瀏覽量
4305
發(fā)布評論請先 登錄
談?wù)?b class='flag-5'>協(xié)程的那些事兒

M1 Mac開發(fā)Android遇到的坑與解決方法
Python中的多核CPU共享數(shù)據(jù)之協(xié)程詳解

Python自動(dòng)化運(yùn)維之協(xié)程函數(shù)賦值過程
Python后端項(xiàng)目的協(xié)程是什么
Python協(xié)程與JavaScript協(xié)程的對比及經(jīng)驗(yàn)技巧
使用channel控制協(xié)程數(shù)量
詳解Linux線程、線程與異步編程、協(xié)程與異步
協(xié)程的概念及協(xié)程的掛起函數(shù)介紹
Kotlin協(xié)程實(shí)戰(zhàn)進(jìn)階之筑基篇2

Kotlin協(xié)程實(shí)戰(zhàn)進(jìn)階之筑基篇3
FreeRTOS任務(wù)與協(xié)程介紹
C++20無棧協(xié)程超輕量高性能異步庫開發(fā)實(shí)戰(zhàn)
協(xié)程的實(shí)現(xiàn)與原理
Linux線程、線程與異步編程、協(xié)程與異步介紹

評論