前言
本文主要基於Kotlin,之前寫過一些Kotlin的文章,比較淺,有興趣的小夥伴可以看上那麼一看
對於Java的小夥伴來說,執行緒可以說是一個又愛又恨的傢伙。執行緒可以帶給我們不阻礙主執行緒的後臺操作,但隨之而來的執行緒安全、執行緒消耗等問題又是我們不得不處理的問題。
對於Java開發來說,合理使用執行緒池可以幫我們處理隨意開啟執行緒的消耗。此外RxJava庫的出現,也幫助我們更好的去執行緒進行切換。所以一直以來執行緒佔據了我的日常開發...
直到,我接觸了協程...
正文
我們們先來看一段Wiki上關於協程(Coroutine)的一些介紹:協程是計算機程式的一類元件,允許執行被掛起與被恢復。但是,到2003年,很多最流行的程式語言,包括C和它的後繼,都未在語言內或其標準庫中直接支援協程。在當今的主流程式設計環境裡,執行緒是協程的合適的替代者...
但是!如今已經2019年了,協程真的沒有用武之地麼?!今天讓我們從Kotlin中感受協程的有趣之處!
一、協程
開始實戰之前,我們聊一聊協程這麼的概念。開啟協程之前,我們先說一說我們們日常中的函式:
函式,在所有語言中都是層級呼叫,比如函式A呼叫函式B,函式B中又呼叫了函式C,函式C執行完畢返回,函式B執行完畢返回,最後是函式A執行完畢。
所以可以看出來函式的呼叫是通過棧實現的。
函式的呼叫總是一個入口,一次return,呼叫順序是明確的。而協程的不同之處就在於,執行過程中函式內部是可中斷的,也就是說中斷之後,可以轉而執行別的函式,在合適的時機再return回來繼續執行沒有執行完的內容。
而這種中斷,叫做掛起。掛起我們當前的函式,再某個合適的時機,才反過來繼續執行~這裡我們再想想回撥:註冊一個回撥函式,在合適的時機執行這個回撥。
- 回撥採用的是一種非同步的形式
- 而協程則是同步
是不是一時有點懵逼。不著急,我們往下看,往下更懵逼,哈哈~
二、Kotlin中的協程
通過Wiki上的介紹,我們不難看出協程是一種標準。任何語言都可以選擇去支援它。
這裡是關於Kotlin中協程的文件:kotlinlang.org/docs/refere…
假設我們想在android中的專案中使用協程該怎麼辦?很簡單。
假設可以已經配好了Kotlin依賴
2.1、gradle引入
在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"
複製程式碼
2.2、基本demo
先看一段官方的基礎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%。
2.2.1、delay()的實現
預警...這裡將會引入大量的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) }
}
複製程式碼
2.3、繼續理解
接下來,我們們來好好理解一下上面程式碼的含義。
首先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執行緒進行執行)。這樣我們就做到在子執行緒啟動協程,完成我們曾經執行緒的樣子...
思考
很多朋友,肯定這裡就產生疑問了。既然還是用子執行緒做後臺任務...那協程存在的意義有是什麼呢?那接下來讓我們們走進協程的意義。
三、協程的作用
3.1、拒絕CallBack
我們日常開發時,經常會遇到這樣的需求:比如一個發文流程中,我們要先登入;登入成功後,我們再進行發文;發文成功後我們更新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程式碼,直來直去,直接順序邏輯寫即可。
沒錯,這就是協程的作用之一。
- 1、當然,很多小夥伴會說Java8引入的Future也可以完成類似的序列執行。(不過,話說回來是不是很多小夥伴沒有升到Java8)...
- 2、肯定也有其他小夥伴說,我可以使用Rx的方式,也能完成這種呼叫...
哈哈,完全沒錯。因為大家都是為了解決同樣的問題,但是協程還有其他好用的地方...
3.2、方便的執行緒切換
想一個我們很常見的需求,子執行緒網路請求,資料回來後切到主執行緒更新UI。
runOnUiThread()
、RxJava都能很方便的幫我們切換執行緒。這裡我們看一下協程的方式:
GlobalScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO){
// 網路請求,並return請求結果
... result
}
// 更新UI
updateUI(result)
}
複製程式碼
很直來直去的邏輯,很直來直去的程式碼。可讀性簡直+100%。
withContext()
可以方便的幫我們在協程的上下文環境中切換執行緒,並返回執行結果。
3.3、方便的併發
我們再來看一段官方程式碼:
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
}
複製程式碼
四、總結
這篇文章,主要是引出協程。協程不是一個新概念,很多語言都支援。
協程,引入了掛起的概念,讓我們的函式可以隨意的暫停,然後在我們原意的時候再執行。通知提供給了我們同步寫非同步程式碼的能力...幫助我們更高效的寫程式碼,更直觀的寫程式碼。
尾聲
關於協程,有很多很多的內容,可以聊。因為篇幅和時間的關係更多的細節,留給我們接下來的文章吧。