老是在聊線程Thread,試試協程吧!

前言

本文主要基於Kotlin,以前寫過一些Kotlin的文章,比較淺,有興趣的小夥伴能夠看上那麼一看html

快速切換至Kotlin for Android模式android

充分理解Kotlin,快速上手寫業務數據庫

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

對於Java開發來講,合理使用線程池能夠幫咱們處理隨意開啓線程的消耗。此外RxJava庫的出現,也幫助咱們更好的去線程進行切換。因此一直以來線程佔據了個人平常開發...api

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

正文

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

可是!現在已經2019年了,協程真的沒有用武之地麼?!今天讓咱們從Kotlin中感覺協程的有趣之處!併發

1、協程

開始實戰以前,咱們聊一聊協程這麼的概念。開啓協程以前,咱們先說一說我們平常中的函數app

函數,在全部語言中都是層級調用,好比函數A調用函數B,函數B中又調用了函數C,函數C執行完畢返回,函數B執行完畢返回,最後是函數A執行完畢。異步

因此能夠看出來函數的調用是經過棧實現的。

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

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

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

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

2、Kotlin中的協程

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

這裏是關於Kotlin中協程的文檔:kotlinlang.org/docs/refere…

假設咱們想在android中的項目中使用協程該怎麼辦?很簡單。

假設能夠已經配好了Kotlin依賴

2.一、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.二、基本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.一、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.三、繼續理解

接下來,我們來好好理解一下上面代碼的含義。

首先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、協程的做用

3.一、拒絕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代碼,直來直去,直接順序邏輯寫便可。

沒錯,這就是協程的做用之一。

  • 一、固然,不少小夥伴會說Java8引入的Future也能夠完成相似的串行執行。(不過,話說回來是否是不少小夥伴沒有升到Java8)...
  • 二、確定也有其餘小夥伴說,我可使用Rx的方式,也能完成這種調用...

哈哈,徹底沒錯。由於你們都是爲了解決一樣的問題,可是協程還有其餘好用的地方...

3.二、方便的線程切換

想一個咱們很常見的需求,子線程網絡請求,數據回來後切到主線程更新UI。

runOnUiThread()、RxJava都能很方便的幫咱們切換線程。這裏咱們看一下協程的方式:

GlobalScope.launch(Dispatchers.Main) {
    val result = withContext(Dispatchers.IO){
        // 網絡請求,並return請求結果
        ... result
    }
    // 更新UI
    updateUI(result)
}
複製代碼

很直來直去的邏輯,很直來直去的代碼。可讀性簡直+100%。

withContext()能夠方便的幫咱們在協程的上下文環境中切換線程,並返回執行結果。

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
}
複製代碼

4、總結

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

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

尾聲

關於協程,有不少不少的內容,能夠聊。由於篇幅和時間的關係更多的細節,留給咱們接下來的文章吧。

我是一個應屆生,最近和朋友們維護了一個公衆號,內容是咱們在從應屆生過渡到開發這一路所踩過的坑,以及咱們一步步學習的記錄,若是感興趣的朋友能夠關注一下,一同加油~

我的公衆號:鹹魚正翻身
相關文章
相關標籤/搜索