本文主要基於Kotlin,以前寫過一些Kotlin的文章,比較淺,有興趣的小夥伴能夠看上那麼一看html
快速切換至Kotlin for Android模式android
對於Java的小夥伴來講,線程能夠說是一個又愛又恨的傢伙。線程能夠帶給咱們不阻礙主線程的後臺操做,但隨之而來的線程安全、線程消耗等問題又是咱們不得不處理的問題。編程
對於Java開發來講,合理使用線程池能夠幫咱們處理隨意開啓線程的消耗。此外RxJava庫的出現,也幫助咱們更好的去線程進行切換。因此一直以來線程佔據了個人平常開發...api
直到,我接觸了協程...安全
我們先來看一段Wiki上關於協程(Coroutine)的一些介紹:協程是計算機程序的一類組件,容許執行被掛起與被恢復。可是,到2003年,不少最流行的編程語言,包括C和它的後繼,都未在語言內或其標準庫中直接支持協程。在當今的主流編程環境裏,線程是協程的合適的替代者...網絡
可是!現在已經2019年了,協程真的沒有用武之地麼?!今天讓咱們從Kotlin中感覺協程的有趣之處!併發
開始實戰以前,咱們聊一聊協程這麼的概念。開啓協程以前,咱們先說一說我們平常中的函數:app
函數,在全部語言中都是層級調用,好比函數A調用函數B,函數B中又調用了函數C,函數C執行完畢返回,函數B執行完畢返回,最後是函數A執行完畢。異步
因此能夠看出來函數的調用是經過棧實現的。
函數的調用老是一個入口,一次return,調用順序是明確的。而協程的不一樣之處就在於,執行過程當中函數內部是可中斷的,也就是說中斷以後,能夠轉而執行別的函數,在合適的時機再return回來繼續執行沒有執行完的內容。
而這種中斷,叫作掛起。掛起咱們當前的函數,再某個合適的時機,才反過來繼續執行~這裏咱們再想一想回調:註冊一個回調函數,在合適的時機執行這個回調。
是否是一時有點懵逼。不着急,咱往下看,往下更懵逼,哈哈~
經過Wiki上的介紹,咱們不難看出協程是一種標準。任何語言均可以選擇去支持它。
這裏是關於Kotlin中協程的文檔:kotlinlang.org/docs/refere…
假設咱們想在android中的項目中使用協程該怎麼辦?很簡單。
假設能夠已經配好了Kotlin依賴
在Android中協程的引入很是的簡單,只須要在gradle中:
apply plugin: 'kotlin-android-extensions'
複製代碼
而後依賴中添加:
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.0"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.0"
複製代碼
先看一段官方的基礎demo:
// 啓動一個協程
GlobalScope.launch(Dispatchers.Main) {
// 執行一個延遲10秒的函數
delay(10000L)
println("World!-${Thread.currentThread().name}")
}
println("Hello-${Thread.currentThread().name}-")
複製代碼
這段代碼執行結果應該你們都能猜到:Hello-main-World!-main
。你們有沒有注意到,這倆個輸出,所有打印了main線程。
這段代碼在主線程執行,而且延遲了10秒鐘,並且也沒有出現ANR!
固然,這裏有小夥伴會說,我能夠經過Handler進行
postDelay()
也能作到這種效果。沒錯,咱們的postDelay()
,是一種回調的解決方案。而咱們開頭提到過,協程使用同步的方式去解決這類問題。
因此,協程中的delay()
也是經過隊列實現的。可是!它用同步的形式屏棄掉了回調,讓咱們的代碼可讀性+100%。
預警...這裏將會引入大量的Kotlin中的協程api。爲了不閱讀不適。這一小節建議直接跳過
跳過總結:
Kotlin爲咱們提供了一些api,幫咱們可以擺脫CallBack,本質也是經過封裝CallBack的形式,實現同步化異步代碼。
public suspend fun delay(timeMillis: Long) {
if (timeMillis <= 0) return // don't delay
// 很明顯能夠看出,實現仍然是用CallBack的形式
return suspendCancellableCoroutine sc@ { cont: CancellableContinuation<Unit> ->
cont.context.delay.scheduleResumeAfterDelay(timeMillis, cont)
}
}
/** Returns [Delay] implementation of the given context */
internal val CoroutineContext.delay: Delay get() = get(ContinuationInterceptor) as? Delay ?: DefaultDelay
internal actual val DefaultDelay: Delay = DefaultExecutor
複製代碼
delay()
使用suspendCancellableCoroutine()
掛起協程,通常狀況下控制協程恢復的關鍵在DefaultExecutor.scheduleResumeAfterDelay()
中,中實現是schedule(DelayedResumeTask(timeMillis, continuation))
,關鍵邏輯是將DelayedResumeTask
放到DefaultExecutor
的隊列最後,在延遲的時間到達就會執行DelayedResumeTask
,那麼該 task 裏面的實現是什麼:
override fun run() {
// 直接在調用者線程恢復協程
with(cont) { resumeUndispatched(Unit) }
}
複製代碼
接下來,我們來好好理解一下上面代碼的含義。
首先delay()
被稱之爲掛起函數,這種函數在協程的做用域中,能夠被掛起,掛起後不阻塞當前線程中協程做用域之外的代碼執行。而且協程會在合適的時機,恢復掛起繼續執行協程做用域中後續的代碼。
而上述代碼中的GlobalScope.launch(Dispatchers.Main) {}
,就是在主線程建立一個全局的協程做用域。而咱們的delay(10000)
是一個掛起函數,執行到它的時候,協程會掛起此函數。讓出CPU,此時咱們協程做用域以外的println("Hello-${Thread.currentThread().name}-")
就有機會執行了。
當合適的時機到來,也就是10000毫秒事後。協程會恢復掛起函數,繼續執行後續的代碼。
看到這,我猜確定有小夥伴,心裏臥槽了一聲:「這不徹底不須要線程了?之後阻塞操做,直接寫在掛起函數了?」。這是徹底錯誤的想法!協程提供的是同步化異步代碼的能力。協程是在用戶態幫咱們封裝了對應的異步api。而不是真正提供了異步的能力。因此若是咱們在主線程的協程中進行IO操做,同樣會阻塞住主線程。
GlobalScope.launch(Dispatchers.Main) {
...網絡請求/...大量數據的數據庫操做
}
複製代碼
同樣會拋出NetworkOnMainThread
/同樣會阻塞主線程。由於上述代碼,本質仍是在主線程執行。因此假設咱們在協程中運行阻塞當前線程的代碼(好比IO操做),仍然會阻塞住當前的線程。也就是有可能出現咱們常見的ANR。
所以,在這種場景下,咱們須要這麼調用:
GlobalScope.launch(Dispatchers.IO) {
...網絡請求/...大量數據的數據庫操做
}
複製代碼
咱們在啓動一個協程的時候,改了一個新的協程上下文(這個上下文會將協程切換到IO線程進行執行)。這樣咱們就作到在子線程啓動協程,完成咱們曾經線程的樣子...
不少朋友,確定這裏就產生疑問了。既然仍是用子線程作後臺任務...那協程存在的意義有是什麼呢?那接下來讓我們走進協程的意義。
咱們平常開發時,常常會遇到這樣的需求:好比一個發文流程中,咱們要先登陸;登陸成功後,咱們再進行發文;發文成功後咱們更新UI。
來段僞碼,簡單實現一下這樣的需求:
// 登陸的僞碼。傳遞一個lambda,也就是一個CallBack
fun login(cb: (User) -> Unit) { ... }
// 發文的僞碼
fun postContent(user: User, content: String, cb: (Result) -> Unit) { ... }
// 更新UI
fun updateUI(result: Result) { ... }
fun ugcPost(content: String) {
login { user ->
postContent(user, content) { result ->
updateUI(result)
}
}
}
複製代碼
這種需求下,咱們一般會由倆個CallBack完成這種串行的需求。不知道你們平常寫這種代碼的時候,有沒有思考過,爲何串行的邏輯,要用**CallBack的形式(異步)**完成?
可能你們會說:這些需求要用線程去進行後臺執行,只能經過CallBack拿到結果。
那麼問題又來了,爲何用線程作後臺邏輯時,咱們就必需要用CallBack呢?畢竟從咱們的思惟邏輯上來講,這些需求就是串行,理論上順序執行代碼就ok了。因此協程的做用就出現了...
這種經過異步形式的邏輯,在協程的輔助下就可變成同步執行:
// 掛起函數,不須要任何CallBack,咱們CallBack的內容,只須要當作返回值return便可
suspend fun login(): User { ... }
suspend fun postContent(user: User, content: String): Result { ... }
fun updateUI(result: Result) { ... }
fun ugcPost(content: String) {
GlobalScope.launch {
val user = login()
val result = postContent(user, content)
updateUI(result)
}
}
複製代碼
這樣咱們就完成了本來須要層層嵌套的CallBack代碼,直來直去,直接順序邏輯寫便可。
沒錯,這就是協程的做用之一。
哈哈,徹底沒錯。由於你們都是爲了解決一樣的問題,可是協程還有其餘好用的地方...
想一個咱們很常見的需求,子線程網絡請求,數據回來後切到主線程更新UI。
runOnUiThread()
、RxJava都能很方便的幫咱們切換線程。這裏咱們看一下協程的方式:
GlobalScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO){
// 網絡請求,並return請求結果
... result
}
// 更新UI
updateUI(result)
}
複製代碼
很直來直去的邏輯,很直來直去的代碼。可讀性簡直+100%。
withContext()
能夠方便的幫咱們在協程的上下文環境中切換線程,並返回執行結果。
咱們再來看一段官方代碼:
import kotlinx.coroutines.*
import kotlin.system.*
fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
val one = doSomethingUsefulOne()
val two = doSomethingUsefulTwo()
println("The answer is ${one + two}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假設咱們在這裏作了些有用的事
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假設咱們在這裏也作了一些有用的事
return 29
}
複製代碼
輸出結果以下: The answer is 42
Completed in 2017 ms
假設咱們耗時計算操做,沒有任何依賴關係。所以最佳的方案,就是讓它們倆並行執行。如何讓doSomethingUsefulOne()
、doSomethingUsefulTwo()
同時執行呢?
答案是:async + await
fun main() = runBlocking<Unit> {
val time = measureTimeMillis {
val one = async { doSomethingUsefulOne() }
val two = async { doSomethingUsefulTwo() }
println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")
}
suspend fun doSomethingUsefulOne(): Int {
delay(1000L) // 假設咱們在這裏作了些有用的事
return 13
}
suspend fun doSomethingUsefulTwo(): Int {
delay(1000L) // 假設咱們在這裏也作了些有用的事
return 29
}
複製代碼
這篇文章,主要是引出協程。協程不是一個新概念,不少語言都支持。
協程,引入了掛起的概念,讓咱們的函數能夠隨意的暫停,而後在咱們原意的時候再執行。通知提供給了咱們同步寫異步代碼的能力...幫助咱們更高效的寫代碼,更直觀的寫代碼。
關於協程,有不少不少的內容,能夠聊。由於篇幅和時間的關係更多的細節,留給咱們接下來的文章吧。