如何搞清楚 Kotlin 協程

【翻譯中】 proandroiddev.com/how-to-make…javascript

協程做爲一種寫異步代碼的偉大方式,它能夠完美的實現異步代碼的可讀性和可維護性。Kotlin提供了單一的語法結構來建立一個異步代碼塊:經過"suspend"關鍵字,和一些配套的函數庫。java

在這篇文章裏,我將試着用簡潔的語言解釋清楚協程和suspending函數的本質。爲了讓本文章不至於太長,我再本文不會深刻講解協程的高級結構。重點是對協程作一個概述,而後分享下我對協程的理解。android

什麼是協程?

Kotlin團隊把協程定義爲"輕量級的線程"。它是一系列能夠在真實的線程中執行的任務。在Kotlin官網,有這樣一幅插圖:express

最有意思的是,線程能夠在一些特定的「暫停點」暫停執行協程,而後去作一些其餘的工做。他能夠在未來的某一時刻從新執行這個協程,甚至可讓其餘的線程來接管這個協程。因此準確的說,協程不只僅是一個「task」,而是一組有序的「子任務」按照指定的順序依次執行。即便看起來在一個順序的代碼塊中,每個對掛起函數的調用都對應啓動一個協程中的新的「子任務」。promise

這就引出了咱們今天討論的主題:掛起方法。併發

掛起方法

你能夠找到許多相似於kotlinx的delay方法和Ktor的HttpClient.post方法。這些方法在返回前須要等待一些任務或者作一些集中的工做。這些方法都用「suspend"關鍵詞標記。異步

suspend fun delay(timeMillis: Long) {...}
suspend fun someNetworkCallReturningValue(): SomeType {
 ...
}

複製代碼

這類方法就被稱爲掛起方法。就像咱們剛纔看到的:async

掛起方法能夠在不阻塞當前線程的狀況下暫停當前協程的執行。這意味着,在調用一個掛起方法的那一刻,當前的代碼會中止執行,而且會在未來的某一時刻從新執行。然而,他並無說當前線程在這期間會作什麼事兒。ide

這時它可能會返回到執行另外一個協程,而後它可能會繼續執行咱們離開的協程。全部這些都由非掛起函數調用掛起函數的方式控制,可是掛起函數自己並無異步性。函數

掛起函數只有在顯示的使用的時候纔是異步的。咱們稍後會介紹。可是如今,您能夠簡單地將掛起函數視爲執行過程須要一些時間的特殊函數。而且隱式將當前函數劃分紅幾個字任務,而不用擔憂線程和任務分發的複雜性。這就是爲何咱們說它很棒,當你在使用它的時候,你不須要擔憂這些。

掛起的世界是有序的

您可能已經注意到,掛起函數沒有特殊的返回類型。它的聲明和普通函數沒有區別。咱們並不須要相似Java的Future或者JavaScript的Promise這樣的包裝類。這進一步證實了掛起函數自己不是異步的,不像JavaScript的異步函數,返回的是promises。

從掛起函數內部,咱們能夠對函數的調用順序進行推理。

這就是爲何在Kotlin中異步的東西很容易推理。在掛起函數內部,對其餘掛起函數的調用與普通函數調用的行爲相似:在獲取返回值並執行其他代碼以前,咱們須要等待被調用函數的執行。

suspend fun someNetworkCallReturningSomething(): Something {
    // some networking operations making use of the suspending mechanism
}

suspend fun someBusyFunction(): Unit {
    delay(1000L)
    println("Printed after 1 second")
    val something: Something = someNetworkCallReturningSomething()
    println("Received $something from network")
}

複製代碼

這將容許咱們稍後以簡單的方式編寫複雜的異步代碼。

鏈接普通世界和掛載世界

在「普通」函數中直接調用掛起函數是不被容許的。一般的解釋是「由於只有協程能夠被掛起」,從這裏咱們得出結論,咱們須要建立一個協程來運行咱們的掛起函數。這很棒。可是爲何呢?

從概念上講,掛起函數在某種程度上從它們的聲明中宣佈它們可能「須要一些時間來執行」。若是您本身不是一個掛起函數,這將強制您顯式地執行如下兩種操做之一:

在等待時阻塞線程(就像普通的同步函數調用同樣)

使用異步方式爲您完成任務,並當即返回(有多種方式來實現)

經過建立協程來實現能夠做爲您的一種選擇,這種選擇必須是顯式的(這很棒!)這是經過使用稱爲協程構建器的函數來實現的。

協程構建器

協程構建器是一個簡單的方法,用來建立一個新的協程,來運行一個掛載方法。它能夠在一個普通的方法裏調用,因爲他們本身沒有被掛起,所以他們能夠充當正常與掛起世界之間的橋樑。

Kotlin標準庫包含了多種協程構造器來構造一系列的協程。咱們會在下面的章節裏介紹其中的幾種。

經過「runBlocking」來阻塞當前的線程。

在一個普通方法裏處理一個掛起方法的最簡單的方式是阻塞當前的線程,而後等待。阻塞當前線程的協程構造器叫作 runBlocking:

fun main() { 
    println("Hello,")
    
    // we create a coroutine running the provided suspending lambda
    // and block the main thread while waiting for the coroutine to finish its execution
    runBlocking {
        // now we are inside a coroutine
        delay(2000L) // suspends the current coroutine for 2 seconds
    }
    
    // will be executed after 2 seconds
    println("World!")
}

複製代碼

在runBlocking的環境下,給定的掛起方法以及他的調用層級會一直有效的阻塞當前的線程,直到它執行完成。

從這個方法的簽名中能夠看出來,傳遞給runBlocking的方法是一個掛起方法,即便runBlocking自己不是可掛載的(它是線程阻塞的)

fun <T> runBlocking( ..., block: suspend CoroutineScope.() -> T
): T {
  ...
}

複製代碼

"runBlocking"常常被用在main()函數裏,用來建立一些頂級協程,而且保持JVM的存活(咱們將在關於結構化併發的那部分介紹中看到這一點)。

經過「launch」,發射而後遺忘

一般狀況下,協程的目的不是爲了阻塞線程,而是爲了啓動一個異步任務。launch協程構建器會在後臺啓動一個協程而且再次期間持續運行。

從Kotlin的官方文檔中,咱們能夠看到下面這個例子:

fun main() { 
    GlobalScope.launch { // launch new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main thread continues here immediately
    runBlocking {     // but this expression blocks the main thread
        delay(2000L)  // ... while we delay for 2 seconds to keep JVM alive
    } 
}

複製代碼

經過註釋咱們能夠知道,這個例子首先會馬上在terminal打印出「Hello,」,過一秒後會打印出「World!」。

注意,爲了達到咱們舉這個例子的目的,看到啓動後究竟發生了什麼,咱們須要以某種方式來阻塞main函數。這是爲什咱們一直用重複使用re-using,是爲了保持JVM的存活。(咱們也能夠用Thread.sleep()來實現,但那樣的話就太不Kotlin了,不是嗎?)

不用擔憂這個GlobalScope對象,我立刻就會講到。

經過「async」異步獲取結果

Here is another coroutine builder called async which allows to perform an asynchronous operation returning a value:

這是另外一個名爲async的協程構建器,它容許執行有返回值的異步操做:

爲了獲得延遲值的結果,async返回一個方便的延遲對象,它相似於Future或Promise。咱們能夠對這個延遲值調用wait,以便等待它執行完並得到結果。

wait不是一個普通的阻塞函數,它是一個掛起函數。這意味着咱們不能直接從main()函數中調用它。爲了等待結果,咱們須要以某種方式阻塞main函數,所以咱們在這裏使用runBlocking來封裝這個await調用。

眼睛犀利如你或許已經注意到了,GlobalScope再次出如今這裏了,由於我在這裏聊聊它了。

結構化的併發性

若是您已經理解了上面的幾個例子,您可能已經注意到咱們須要熟悉經典的「block and wait for my coroutines to finish」模式。

在Java中,這一般是經過保持對線程的引用並對全部線程調用join來得到的,以便在等待全部其餘線程時阻塞主線程。咱們能夠用Kotlin協程作相似的事情,但這不是Kotlin的習慣用法。

在Kotlin中,能夠在層次結構中建立協做程序,這容許父協做程序爲您自動管理其子協做程序的生命週期。例如,它能夠等待其子節點完成,或者在其中一個異常中發生時取消全部子節點。

建立協程的層次結構

除了不該該從協程調用runblock以外,全部協程構建器都聲明爲CoroutineScope類的擴展,以鼓勵人們構造協程:

fun <T> runBlocking(...): T {...}fun <T> CoroutineScope.async(...): Deferred<T> {...}
fun <T> CoroutineScope.launch(...): Job {...}
fun <E> CoroutineScope.produce(...): ReceiveChannel<E> {...}
...

複製代碼

爲了建立一個協同程序,您要麼須要在GlobalScope上調用這些構建器(建立頂級協同程序),要麼須要從一個已經存在的協同程序範圍(建立該範圍的子協同程序)調用這些構建器。事實上,若是您編寫一個建立協程的函數,您也應該將它聲明爲CoroutineScope類的擴展。這是一種約定,容許您輕鬆調用coroutine構建器,由於您能夠這樣使用CoroutineScope。

If you take a look at coroutine builders’ signatures, you may notice that the suspending function they take as a parameter is also defined as an extension function of the CoroutineScope class:

若是你仔細看下協程構建器的簽名,你會發現,被當作參數的掛載方法也是CoroutineScope類的一個擴展:

fun <T> CoroutineScope.async( ... block: suspend CoroutineScope.() -> T
): Deferred<T> {
    ...
}

複製代碼

這意味着咱們能夠在該函數中調用其餘協程構建器而不指定任何接收器,隱式接收器將是當前協程的子範圍,使其充當父進程。這很Easy吧!

下面是咱們應該如何用更習慣的方式來組織前面的例子:

fun main() = runBlocking {
    val deferredResult = async {
        delay(1000L)
        "World!"
    }
    println("Hello, ${deferredResult.await()}")
}

複製代碼
fun main() = runBlocking { 
    launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}

複製代碼
fun main() = runBlocking {
    delay(1000L)
    println("Hello, World!")
}

複製代碼

注意,咱們再也不須要GlobalScope,由於範圍是由包裝runBlocking調用提供的。咱們也不須要額外的延遲來等待子協同程序完成。runblock將等待它的全部子線程完成,而後再完成它本身的執行,所以根據runblock的定義,主線程也將保持阻塞狀態。

coroutineScope構建器

您可能已經注意到,不鼓勵在協程內部使用runBlocking。這是由於Kotlin團隊但願避免協同程序中的線程阻塞函數,而是使用掛起操做。與runBlocking等價的掛起機制是coroutineScope構建器。

coroutineScope只是掛起當前的協同程序,直到全部子協同程序都執行完畢。下面是直接取自Kotlin文檔的例子:

fun main() = runBlocking { // this: CoroutineScope
    launch { 
        delay(200L)
        println("Task from runBlocking")
    }
    
    coroutineScope { // Creates a new coroutine scope
        launch {
            delay(500L) 
            println("Task from nested launch")
        }
    
        delay(100L)
        println("Task from coroutine scope") // This line will be printed before nested launch
    }
    
    println("Coroutine scope is over") // This line is not printed until nested launch completes
}

複製代碼

我在這裏講解的基本構建塊實際上並非Kotlin中協程概念的最重要部分。咱們能夠經過使用通道、生產者和消費者等,利用協同程序來很好地表達併發的東西。但我相信,在開始構建更高抽象以前,咱們首先須要理解這些構建塊。

關於協程還有不少要說的,固然這篇文章只是觸及皮毛,可是我但願這篇文章能幫助您更好地理解協程和掛起函數。

若是個人這篇文章對您有幫助的話,請告訴我,若是你想更深刻的瞭解某一方面的知識的話,也請告訴我。若是你發現本文的一些錯誤,不要猶豫,請必定指出來。

相關文章
相關標籤/搜索