協程的概念在編程語言的早期就出現了,在1967年Simula第一次使用協程。 協程就像很是輕量級的線程。 線程是由系統調度的,線程切換或線程阻塞的開銷都比較大。而協程依賴於線程,可是協程掛起時不須要阻塞線程,幾乎是無代價的,協程是由開發者控制的。因此協程也像用戶態的線程,很是輕量級,一個線程中能夠建立任意個協程。html
舉個你妹子都聽得懂的栗子,不必定很準確。假如要從 地鐵A站 去 地鐵C站 和你妹子約會,可是當到達 地鐵B站 的時候,你想來想去應該去給妹子買個精美的禮物,大概要1個小時,而從 A站 到 C站 只有這一輛列車,只不過開的飛快,每10分鐘又回到 A站從新出發,地鐵比如一條線程,你去買禮物回到B站比如一個任務。在同步阻塞的狀況下,是你去買禮物這段時間,地鐵一直等你,直到你帶着禮物回來。在有協程的狀況下,你去買禮物比如一段協程,地鐵把你在B站放下(掛起),地鐵繼續往前開,你買好禮物了就在B站等下趟地鐵來,上車繼續(恢復)前去和妹子約會。在異步的狀況下是,你去買禮物(異步任務),地鐵繼續往前開,可是地鐵司機給你一個電話號碼(callback),你買禮物回到B站的時候須要打個人電話號碼,才讓你上車。異步callback的時候有個問題,每一個人下車去臨時辦事司機還要給他一個電話號碼,若是他出異常不回來了,可能會致使司機的電話號碼泄露,很是不安全。android
在Android開發中,常常遇到的問題:git
在Android中主線程主要用戶UI的渲染和響應用戶手勢交互,以及輕量級的邏輯運算。若果在主線程發起一個請求,將會致使應用變慢、變卡、沒法響應用戶的交互,很容易形成ARN,用戶體驗極差。因此業界通行的作法是經過callback實現異步回調:github
class ViewModel: ViewModel() {
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
}
複製代碼
上面callback的示例只是一層回調的狀況,假若有兩個甚至更多的異步請求,並且存在下一個請求依賴上一個請求的結果,就會存在層層嵌套,固然目前比較流行的作法是用Retrofit的轉換函數flatMap實現鏈式調用,可是代碼看起來仍是很臃腫。若是使用協程上面的代碼能夠簡化成這樣:編程
// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}
suspend fun get(url: String) = withContext(Dispatchers.IO) {
// Make a request
// Dispatchers.IO
}
複製代碼
Coroutines提供一個很好途徑能夠簡化耗時任務的代碼編寫,使得異步callback的代碼能夠像同步代碼同樣順序編寫。Coroutines在普通的方法上面加上兩個新的操做。除了call 並 return,Coroutines還增長 suspend 和 resume。 協程使用棧幀管理當前運行的方法和方法的全部本地變量。當協程開始掛起,當前棧幀被複制並保存以供後續使用。當協程開始被恢復,棧幀將從它被保存的地方恢復回來,當前棧幀的方法繼續執行。api
suspend functions
只能在協程或者suspend functions
中被調用。安全
在Kotlin協程中,寫的好的suspend functions
老是應該能夠安全的從主線程被調用,也應該容許從任何線程被調用。使用suspend
修飾的 function
並非告訴Kotlin這個方法在主線程運行。bash
爲了寫一個主線程安全的耗時方法,你可讓協程在Default
或者 IO
dispatcher中執行(用withContext(Dispatchers.IO)
指定在IO線程中運行)。在協程全部的協程必須運行在dispatcher中,即便他們運行在主線程中。Coroutines將會掛起本身,dispatcher知道如何恢復他們。網絡
爲了指定coroutines在什麼線程運行,kotlin提供了四種Dispatchers:併發
Dispatchers | 用途 | 使用場景 |
---|---|---|
Dispatchers.Main | 主線程,和UI交互,執行輕量任務 | 1.call suspend functions 。2. call UI functions 。 3. Update LiveData |
Dispatchers.IO | 用於網絡請求和文件訪問 | 1. Database 。 2.Reading/writing files 。3. Networking |
Dispatchers.Default | CPU密集型任務 | 1. Sorting a list 。 2.Parsing JSON 。 3.DiffUtils |
Dispatchers.Unconfined | 不限制任何制定線程 | 高級調度器,不該該在常規代碼裏使用 |
假如你在Room中使用
suspend functions
、RxJava
、LiveData
,它自動提供了主線程安全。
// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}
// Dispatchers.Main
suspend fun get(url: String) =
// Dispatchers.IO
withContext(Dispatchers.IO) {
// Dispatchers.IO
/* perform blocking network IO here */
}
// Dispatchers.Main
複製代碼
你的程序裏面可能會有成千上萬個協程,你很難經過代碼手動追蹤它們,假如你經過代碼手動追蹤他們以確保它們完成或取消,那麼代碼會顯得臃腫且很容易出錯。若是代碼不是很完美,可能會失去對coroutine的追蹤,並致使任務泄露。任務泄露就像內存泄露,可是更嚴重。它不但會浪費內存的使用,還有cpu、磁盤,甚至會發起一個網絡請求。
在android中,咱們知道Activity和Fragment等都是有生命週期的,咱們一般的模式是當前頁面退出的時候,取消全部的異步任務。假若有一個異步的網絡請求,在當前頁面銷燬的時候還在執行,會致使哪些問題:
爲了不協程泄露,kotlin引入 結構化併發 。結構化併發是語言特性和最佳實踐的組合,若是咱們遵循最佳實踐,將幫助追蹤運行在協程中的任務。在Android中結構化併發能夠幫咱們作以下三件事:
解決方式:
CoroutineScope
取消任務,實際上是經過關聯的job取消任務。coroutineScope
和 supervisorScope
保證 suspend function
的全部任務完成才返回。coroutineScope
保證錯誤雙向傳遞,只要有一個子coroutine失敗或出現異常,異常往父域傳遞,並取消全部的子coroutines。而 supervisorScope
實現單向錯誤傳遞,適用於做業監控器。在kotlin中全部的協程必須運行在CoroutineScope中,scope幫你追蹤全部協程的狀態,可是它不像Dispatcher,並不運行你的協程。它能夠取消全部在裏面啓動的協程。啓動一個新協程:
scope.launch {
// This block starts a new coroutine
// "in" the scope.
//
// It can call suspend functions
fetchDocs()
}
複製代碼
建立CoroutineScope
的常見方式以下:
CoroutineScope(context: CoroutineContext)
,api方法,如:val scope = CoroutineScope(Dispatchers.Main + Job())
,或者以下:class LifecycleCoroutineScope : CoroutineScope, Closeable {
private val job = JobSupervisorJob()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
override fun close() {
job.cancel()
}
}
複製代碼
class SimpleRetrofitActivity : FragmentActivity() {
private val activityScope = LifecycleCoroutineScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_simple_retrofit)
// some other code ...
}
override fun onDestroy() {
super.onDestroy()
activityScope.close()
}
// some other code ...
}
複製代碼
coroutineScope
:api方法,建立新一個子域,並管理域中的全部協程。注意這個方法只有在block中建立的全部子協程所有執行完畢後,纔會退出。supervisorScope
:與 coroutineScope
的區別是在子協程失敗時,錯誤不會往上傳遞給父域,因此不會影響子協程。建立協程的常見方式以下:
lauch
:協程構建器,建立並啓動(也能夠延時啓動)一個協程,返回一個Job,用於監督和取消任務,用於無返回值的場景。async
:協程構建器,和launch同樣,區別是返回一個Job的子類 Deferred
,惟一的區別是能夠經過await獲取完成時的返回值,或者捕獲異常(異常處理也不同)。在Android中有一個kotlin的ViewModel的擴展庫 lifecycle-viewmodel-ktx:2.1.0-alpha04
,能夠經過viewModelScope
擴展屬性啓動協程,viewModelScope
綁定了activity的生命週期,activity銷燬的時候會自動取消在這個scope中啓動的全部協程。
fun runForever() {
// start a new coroutine in the ViewModel
viewModelScope.launch {
// cancelled when the ViewModel is cleared
while(true) {
delay(1_000)
// do something every second
}
}
}
複製代碼
注意,協程的取消是協做的,當協程掛起的時候被取消將會拋一個
CancellationException
,即便你捕獲了這個異常,這個協程的狀態也變爲取消狀態。假如你是一個計算協程,而且沒有檢查取消狀態,那麼這個協程不能被取消。
val startTime = System.currentTimeMillis()
val job = launch(Dispatchers.Default) {
var nextPrintTime = startTime
var i = 0
while (i < 5) { // computation loop, just wastes CPU
// print a message twice a second
if (System.currentTimeMillis() >= nextPrintTime) {
println("job: I'm sleeping ${i++} ...")
nextPrintTime += 500L
}
}
}
delay(1300L) // delay a bit
println("main: I'm tired of waiting!")
job.cancelAndJoin() // cancels the job and waits for its completion
println("main: Now I can quit.")
複製代碼
有時,咱們但願兩個或多個請求同時併發,並等待他們所有完成,suspend function
加上 coroutineScope
建立的子域能夠保證所有子協程完成才返回。
suspend fun fetchTwoDocs() {
coroutineScope {
launch { fetchDoc(1) }
async { fetchDoc(2) }
}
}
複製代碼
注意協程的結構化併發是基於語言特性加上最佳實踐的,以下方式會致使,錯誤丟失:
val unrelatedScope = MainScope()
// example of a lost error
suspend fun lostError() {
// async without structured concurrency
unrelatedScope.async {
throw InAsyncNoOneCanHearYou("except")
}
}
複製代碼
上面代碼丟失錯誤是由於 async
的恢復須要調用await
,這樣才能將異常從新上傳,而在suspend function
使用了另一個協程域,致使lostError
不會等待自做業的完成就退出了。正確的結構化併發:
suspend fun foundError() {
coroutineScope {
async {
throw StructuredConcurrencyWill("throw")
}
}
}
複製代碼
你能夠經過
CoroutineScope
(注意是大寫開頭的C
) 和GlobalScope
來建立 非結構化的協程,僅僅當你認爲它的生命週期比調用者生命週期更長。
CoroutineScope
:協程做用域包含 CoroutineContext
,用於啓動協程,並追蹤子協程,實際上是經過Job追蹤的。CoroutineContext
:協程上下文,主要包含Job
和CoroutineDispatcher
,表示一個協程的場景。CoroutineDispatcher
:協程調度器,決定協程所在的線程或線程池。它能夠指定協程運行於特定的一個線程、一個線程池或者不指定任何線程。Job
:任務,封裝了協程中須要執行的代碼邏輯。Job 能夠取消而且有簡單生命週期,它有三種狀態:isActive
、isCompleted
、isCancelled
。Deferred
:Job的子類,有返回值的Job,經過await
獲取。lauch
、async
。