為什么要搞出和用協程呢
是節省CPU,避免系統內核級的線程頻繁切換,造成的CPU資源浪費。好鋼用在刀刃上。而協程是用戶態的線程,用戶可以自行控制協程的創建于銷毀,極大程度避免了系統級線程上下文切換造成的資源浪費。
是節約內存,在64位的Linux中,一個線程需要分配8MB棧內存和64MB堆內存,系統內存的制約導致我們無法開啟更多線程實現高并發。而在協程編程模式下,可以輕松有十幾萬協程,這是線程無法比擬的。
是穩定性,前面提到線程之間通過內存來共享數據,這也導致了一個問題,任何一個線程出錯時,進程中的所有線程都會跟著一起崩潰。
是開發效率,使用協程在開發程序之中,可以很方便的將一些耗時的IO操作異步化,例如寫文件、耗時IO請求等。
對于協程的一個總結
特征:協程是運行在單線程中的并發程序
優點:省去了傳統 Thread 多線程并發機制中切換線程時帶來的線程上下文切換、線程狀態切換、Thread 初始化上的性能損耗,能大幅度的提高并發性能
簡單理解:在單線程上由程序員自己調度運行的并行計算
寫到最后
協程本身不是替換線程的,因為協程是建立在線程之上的,但是協程能夠更好的為我們提供執行高并發任務
1.kotlin中協程的特點:可以用同步的方式寫出異步的代碼
coroutineScope.launch(Dispatchers.Main){// 開始協程:主線程
? ? val token=api.getToken()// 網絡請求:IO 線程
? ? val user=api.getUser(token)// 網絡請求:IO 線程
? ? nameTv.text=user.name// 更新 UI:主線程
}
2.協程中掛起的本質
啟動一個協程可以使用 launch 或者 async 函數,協程其實就是這兩個函數中閉包的代碼塊。
launch ,async 或者其他函數創建的協程,在執行到某一個 suspend 函數的時候,這個協程會被「suspend」,也就是被掛起。
3.協程的代碼塊中,線程執行到了 suspend 函數這里的時候,就暫時不再執行剩余的協程代碼,跳出協程的代碼塊。
那線程接下來會做什么呢?
如果它是一個后臺線程:
要么無事可做,被系統回收
要么繼續執行別的后臺任務
跟 Java 線程池里的線程在工作結束之后是完全一樣的:回收或者再利用。
如果這個線程它是 Android 的主線程,那它接下來就會繼續回去工作:也就是一秒鐘 60 次的界面刷新任務。
一個常見的場景是,獲取一個圖片,然后顯示出來:
// 主線程中
GlobalScope.launch(Dispatchers.Main){
? ? valimage=suspendingGetImage(imageId)// 獲取圖片
? ? avatarIv.setImageBitmap(image)// 顯示出來
}
suspend fun suspendingGetImage(id:String)=withContext(Dispatchers.IO){...}
協程:
線程的代碼在到達suspend函數的時候被掐斷,接下來協程會從這個suspend函數開始繼續往下執行,不過是在指定的線程。
誰指定的?是suspend函數指定的,比如我們這個例子中,函數內部的withContext傳入的Dispatchers.IO所指定的 IO 線程。
Dispatchers調度器,它可以將協程限制在一個特定的線程執行,或者將它分派到一個線程池,或者讓它不受限制地運行,關于Dispatchers這里先不展開了。
那我們平日里常用到的調度器有哪些?
常用的Dispatchers,有以下三種:
Dispatchers.Main:Android 中的主線程
Dispatchers.IO:針對磁盤和網絡 IO 進行了優化,適合 IO 密集型的任務,比如:讀寫文件,操作數據庫以及網絡請求
Dispatchers.Default:適合 CPU 密集型的任務,比如計算
回到我們的協程,它從suspend函數開始脫離啟動它的線程,繼續執行在Dispatchers所指定的 IO 線程。
緊接著在suspend函數執行完成之后,協程為我們做的最爽的事就來了:會自動幫我們把線程再切回來。
這個「切回來」是什么意思?
我們的協程原本是運行在主線程的,當代碼遇到 suspend 函數的時候,發生線程切換,根據Dispatchers切換到了 IO 線程;
當這個函數執行完畢后,線程又切了回來,「切回來」也就是協程會幫我再post一個Runnable,讓我剩下的代碼繼續回到主線程去執行。
我們從線程和協程的兩個角度都分析完成后,終于可以對協程的「掛起」suspend 做一個解釋:
協程在執行到有 suspend 標記的函數的時候,會被 suspend 也就是被掛起,而所謂的被掛起,就是切個線程;
不過區別在于,掛起函數在執行完成之后,協程會重新切回它原先的線程。
再簡單來講,在 Kotlin 中所謂的掛起,就是一個稍后會被自動切回來的線程調度操作。
這個「切回來」的動作,在 Kotlin 里叫做 resume,恢復。
通過剛才的分析我們知道:掛起之后是需要恢復。
而恢復這個功能是協程的,如果你不在協程里面調用,恢復這個功能沒法實現,所以也就回答了這個問題:為什么掛起函數必須在協程或者另一個掛起函數里被調用。
再細想下這個邏輯:一個掛起函數要么在協程里被調用,要么在另一個掛起函數里被調用,那么它其實直接或者間接地,總是會在一個協程里被調用的。
所以,要求suspend函數只能在協程里或者另一個 suspend 函數里被調用,還是為了要讓協程能夠在suspend函數切換線程之后再切回
通過剛才的分析我們知道:掛起之后是需要恢復。
而恢復這個功能是協程的,如果你不在協程里面調用,恢復這個功能沒法實現,所以也就回答了這個問題:為什么掛起函數必須在協程或者另一個掛起函數里被調用。
再細想下這個邏輯:一個掛起函數要么在協程里被調用,要么在另一個掛起函數里被調用,那么它其實直接或者間接地,總是會在一個協程里被調用的。
所以,要求suspend函數只能在協程里或者另一個 suspend 函數里被調用,還是為了要讓協程能夠在 suspend 函數切換線程之后再切回來。
什么是「非阻塞式掛起」
非阻塞式是相對阻塞式而言的。
編程語言中的很多概念其實都來源于生活,就像脫口秀的段子一樣。
線程阻塞很好理解,現實中的例子就是交通堵塞,它的核心有 3 點:
前面有障礙物,你過不去(線程卡了)
需要等障礙物清除后才能過去(耗時任務結束)
除非你繞道而行(切到別的線程)
從語義上理解「非阻塞式掛起」,講的是「非阻塞式」這個是掛起的一個特點,也就是說,協程的掛起,就是非阻塞式的,協程是不講「阻塞式的掛起」的概念的。
我們講「非阻塞式掛起」,其實它有幾個前提:并沒有限定在一個線程里說這件事,因為掛起這件事,本來就是涉及到多線程。
就像視頻里講的,阻塞不阻塞,都是針對單線程講的,一旦切了線程,肯定是非阻塞的,你都跑到別的線程了,之前的線程就自由了,可以繼續做別的事情了。
所以「非阻塞式掛起」,其實就是在講協程在掛起的同時切線程這件事情。
為什么要講非阻塞式掛起
「非阻塞式掛起」和第二篇的「掛起要切線程」是同一件事情,那還有講的必要嗎?
是有的。因為它在寫法上和單線程的阻塞式是一樣的。
協程只是在寫法上「看起來阻塞」,其實是「非阻塞」的,因為在協程里面它做了很多工作,其中有一個就是幫我們切線程。
之前說的掛起,重點是說切線程先切過去,然后再切回來。
而這里的非阻塞式,重點是說線程雖然會切,但寫法上和普通的單線程差不多。
讓我們來看看下面的例子:
main{
GlobalScope.launch(Dispatchers.Main){// 耗時操作val user=suspendingRequestUser()
updateView(user)
}
private suspend fun suspendingRequestUser():User=withContext(Dispatchers.IO){api.requestUser()}}
阻塞的本質
首先,所有的代碼本質上都是阻塞式的,而只有比較耗時的代碼才會導致人類可感知的等待,比如在主線程上做一個耗時 50 ms 的操作會導致界面卡掉幾幀,這種是我們人眼能觀察出來的,而這就是我們通常意義所說的「阻塞」。
舉個例子,當你開發的 app 在性能好的手機上很流暢,在性能差的老手機上會卡頓,就是在說同一行代碼執行的時間不一樣。
視頻中講了一個網絡 IO 的例子,IO 阻塞更多是反映在「等」這件事情上,它的性能瓶頸是和網絡的數據交換,你切多少個線程都沒用,該花的時間一點都少不了。
而這跟協程半毛錢關系沒有,切線程解決不了的事情,協程也解決不了
總結
關于這邊文章的標題協程是什么、掛起是什么、掛起的非阻塞式可以做下面的總結
協程是什么
協程就是切線程;
掛起是什么
掛起就是可以自動切回來的切線程;
掛起的非阻塞式
掛起的非阻塞式指的是它能用看起來阻塞的代碼寫出非阻塞的操作。
2.在kotlin使用協程
項目中配置對 Kotlin 協程的支持
在使用協程之前,我們需要在 build.gradle 文件中增加對 Kotlin 協程的依賴:
項目根目錄下的 build.gradle :
buildscript {
? ? ext.kotlin_coroutines = '1.4.0'
}
Module 下的 build.gradle
dependencies {
? ? ? ? implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.0'
}
創建協程
kotlin 中 GlobalScope 類提供了幾個攜程構造函數:
launch - 創建協程
async - 創建帶返回值的協程,返回的是 Deferred 類
withContext - 不創建新的協程,指定協程上運行代碼塊
runBlocking - 不是 GlobalScope 的 API,可以獨立使用,區別是 runBlocking 里面的 delay 會阻塞線程,而 launch 創建的不會
先跑起來一個簡單的例子:
import kotlinx.coroutines.*
fun main(){
? ? ? ? GlobalScope.launch{
? ? ? ? // 在后臺啟動一個新的協程并繼續
? ? ? ? delay(1000L)// 非阻塞的等待 1 秒鐘(默認時間單是毫秒)
? ? ? ? println("World!")// 在延遲后打印輸出}
? ? ? ? println("Hello,")// 協程已在等待時主線程還在繼續
? ? ? ? Thread.sleep(2000L)// 阻塞主線程 2 秒鐘來保證 JVM 存活
}
協程中卻有一個很實用的函數:withContext 。這個函數可以切換到指定的線程,并在閉包內的邏輯執行結束之后,自動把線程切回去繼續執行。那么可以將上面的代碼寫成這樣:
coroutineScope.launch(
? ? Dispatchers.Main){//? 在 UI 線程開始
? ? val image=withContext(Dispatchers.IO){// 切換到 IO 線程,并在執行完成后切回 UI 線程
? ? getImage(imageId)// 將會運行在 IO 線程}
? ? avatarIv.setImageBitmap(image)// 回到 UI 線程更新 UI
}
我們甚至可以把 withContext 放進一個單獨的函數里面:
launch(Dispatchers.Main){//? 在 UI 線程開始
val image=getImage(imageId)
avatarIv.setImageBitmap(image)//? 執行結束后,自動切換回 UI 線程}//
suspend fun getImage(imageId:Int)=withContext(Dispatchers.IO){...}
launch 函數
launch 函數定義
public fun CoroutineScope.launch(
? ? context:CoroutineContext=EmptyCoroutineContext,
? ? start:CoroutineStart=CoroutineStart.DEFAULT,
? ? block:suspendCoroutineScope.()->Unit):Job
launch 是個擴展函數,接受3個參數,前面2個是常規參數,最后一個是個對象式函數,這樣的話 kotlin 就可以使用以前說的閉包的寫法:() 里面寫常規參數,{} 里面寫函數式對象的實現,就像上面的例子一樣
我們需要關心的是 launch 的3個參數和返回值 Job:
CoroutineContext - 可以理解為協程的上下文,在這里我們可以設置 CoroutineDispatcher 協程運行的線程調度器,有 4種線程模式:
Dispatchers.Default
Dispatchers.IO -
Dispatchers.Main - 主線程
Dispatchers.Unconfined - 沒指定,就是在當前線程
不寫的話就是 Dispatchers.Default 模式的,或者我們可以自己創建協程上下文,也就是線程池,newSingleThreadContext 單線程,newFixedThreadPoolContext 線程池
CoroutineStart - 啟動模式,默認是DEAFAULT,也就是創建就啟動;還有一個是LAZY,意思是等你需要它的時候,再調用啟動
DEAFAULT - 模式模式,不寫就是默認
ATOMIC -
UNDISPATCHED
LAZY - 懶加載模式,你需要它的時候,再調用啟動
block - 閉包方法體,定義協程內需要執行的操作
Job - 協程構建函數的返回值,可以把 Job 看成協程對象本身,協程的操作方法都在 Job 身上了
job.start() - 啟動協程,除了 lazy 模式,協程都不需要手動啟動
job.join() - 等待協程執行完畢
job.cancel() - 取消一個協程
job.cancelAndJoin() - 等待協程執行完畢然后再取消
創建一個協程
創建該launch函數返回了一個可以被用來取消運行中的協程的Job
val job=launch{
? ? repeat(1000){i->
? ? ? ? println("job: I'm sleeping$i...")
? ? ? ? delay(500L)
? ? }
}
取消
val? job=launch{
repeat(1000){
i->println("job: I'm sleeping $i ...")
delay(500L)}}
delay(1300L)// 延遲一段時間
println("main: I'm tired of waiting!")job.cancel()// 取消該作業
job.join()// 等待作業執行結束
println("main: Now I can quit.")
程序執行后的輸出如下:
job: I'm sleeping 0 ...
job: I'm sleeping 1 ...
job: I'm sleeping 2 ...
main: I'm tired of waiting!
main: Now I can quit.
一旦 main 函數調用了 job.cancel,我們在其它的協程中就看不到任何輸出,因為它被取消了
超時
在實踐中絕大多數取消一個協程的理由是它有可能超時。 當你手動追蹤一個相關 Job的引用并啟動了一個單獨的協程在延遲后取消追蹤,這里已經準備好使用 withTimeout 函數來做這件事。 來看看示例代碼:
withTimeout(1300L){repeat(1000){i->
println("I'm sleeping $i ...")
delay(500L)}}
運行后得到如下輸出:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
這里我們看到了TimeoutCancellationException異常,這異常是因為超時導致的異常取消
解決這個問題也很簡單通過withTimeout的withTimeoutOrNull函數,代碼示例:
valresult=withTimeoutOrNull(1300L){repeat(1000){i->println("I'm sleeping$i...")delay(500L)}"Done"http:// 在它運行得到結果之前取消它}println("Result is$result")
運行后得到如下輸出:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Result is null
這樣就沒有拋出異常了
async 函數定義
public fun <T> CoroutineScope.async(
? ? context:CoroutineContext=EmptyCoroutineContext,
? ? start:CoroutineStart=CoroutineStart.DEFAULT,
? ? block:suspendCoroutineScope.()->T):Deferred<T>{
? ? ? ? val? newContext=newCoroutineContext(context)
? ? ? ? val coroutine=if(start.isLazy){
? ? ? ? LazyDeferredCoroutine(newContext,block)
}else{DeferredCoroutine<T>(newContext,active=true)
? ? ? ? coroutine.start(start,coroutine,block)
? ? ? ? return coroutine
}
從源碼可以看出launch 和 async的唯一區別在于async的返回值
async 返回的是 Deferred 類型,Deferred 繼承自 Job 接口,Job有的它都有,增加了一個方法 await ,這個方法接收的是 async 閉包中返回的值,async 的特點是不會阻塞當前線程,但會阻塞所在協程,也就是掛起
但是需要注意的是async 并不會阻塞線程,只是阻塞鎖調用的協程
async和launch的區別
launch 更多是用來發起一個無需結果的耗時任務,這個工作不需要返回結果。
async 函數則是更進一步,用于異步執行耗時任務,并且需要返回值(如網絡請求、數據庫讀寫、文件讀寫),在執行完畢通過 await() 函數獲取返回值。
runBlocking
runBlocking啟動的協程任務會阻斷當前線程,直到該協程執行結束。當協程執行結束之后,頁面才會被顯示出來。
runBlocking 通常適用于單元測試的場景,而業務開發中不會用到這個函數
relay、yield
relay 和 yield 方法是協程內部的操作,可以掛起協程,
relay、yield 的區別
relay 是掛起協程并經過執行時間恢復協程,當線程空閑時就會運行協程
yield 是掛起協程,讓協程放棄本次 cpu 執行機會讓給別的協程,當線程空閑時再次運行協程。
我們只要使用 kotlin 提供的協程上下文類型,線程池是有多個線程的,再次執行的機會很快就會有的。
除了 main 類型,協程在掛起后都會封裝成任務放到協程默認線程池的任務隊列里去,有延遲時間的在時間過后會放到隊列里去,沒有延遲時間的直接放到隊列里去
原文鏈接:http://www.lxweimin.com/p/402a69dbd66d