總是在聊線程,試試協程吧:基礎閱讀篇

Kotlin Gradle Java 讀書 Android 編程語言 碼農登陸 2019-06-27
總是在聊線程,試試協程吧:基礎閱讀篇

前言

對於Java的小夥伴來說,線程可以說是一個又愛又恨的傢伙。線程可以帶給我們不阻礙主線程的後臺操作,但隨之而來的線程安全、線程消耗等問題又是我們不得不處理的問題。

對於Java開發來說,合理使用線程池可以幫我們處理隨意開啟線程的消耗。此外RxJava庫的出現,也幫助我們更好的去線程進行切換。所以一直以來線程佔據了我的日常開發...

直到,我接觸了協程...

正文

咱們先來看一段Wiki上關於協程(Coroutine)的一些介紹:協程是計算機程序的一類組件,允許執行被掛起與被恢復。但是,到2003年,很多最流行的編程語言,包括C和它的後繼,都未在語言內或其標準庫中直接支持協程。在當今的主流編程環境裡,線程是協程的合適的替代者...

但是!如今已經2019年了,協程真的沒有用武之地麼?!今天讓我們從Kotlin中感受協程的有趣之處!

一、協程

開始實戰之前,我們聊一聊協程這麼的概念。開啟協程之前,我們先說一說咱們日常中的函數

函數,在所有語言中都是層級調用,比如函數A調用函數B,函數B中又調用了函數C,函數C執行完畢返回,函數B執行完畢返回,最後是函數A執行完畢。

所以可以看出來函數的調用是通過棧實現的。

函數的調用總是一個入口,一次return,調用順序是明確的。而協程的不同之處就在於,執行過程中函數內部是可中斷的,也就是說中斷之後,可以轉而執行別的函數,在合適的時機再return回來繼續執行沒有執行完的內容。

而這種中斷,叫做掛起。掛起我們當前的函數,再某個合適的時機,才反過來繼續執行~這裡我們再想想回調:註冊一個回調函數,在合適的時機執行這個回調。

  • 回調採用的是一種異步的形式
  • 協程則是同步

是不是一時有點懵逼。不著急,咱往下看,往下更懵逼,哈哈~

二、Kotlin中的協程

通過Wiki上的介紹,我們不難看出協程是一種標準。任何語言都可以選擇去支持它。

這裡是關於Kotlin中協程的文檔:https://kotlinlang.org/docs/reference/coroutines-overview.html

假設我們想在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

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。

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
}

四、總結

這篇文章,主要是引出協程。協程不是一個新概念,很多語言都支持。

協程,引入了掛起的概念,讓我們的函數可以隨意的暫停,然後在我們原意的時候再執行。通知提供給了我們同步寫異步代碼的能力...幫助我們更高效的寫代碼,更直觀的寫代碼。

尾聲

關於協程,有很多很多的內容,可以聊。因為篇幅和時間的關係更多的細節,留給我們接下來的文章吧。

相關推薦

推薦中...