【譯】kotlin 協程官方文檔(1)-協程基礎(Coroutine Basics)

最近一直在瞭解關於kotlin協程的知識,那最好的學習資料天然是官方提供的學習文檔了,看了看後我就萌生了翻譯官方文檔的想法。先後花了要接近一個月時間,一共九篇文章,在這裏也分享出來,但願對讀者有所幫助。我的知識所限,有些翻譯得不是太順暢,也但願讀者能提出意見git

協程官方文檔:coroutines-guidegithub

協程官方文檔中文翻譯:coroutines-cn-guideexpress

協程官方文檔中文譯者:leavesC安全

[TOC]bash

此章節涵蓋了協程的基本概念併發

1、你的第一個協程程序(Your first coroutine)

運行如下代碼:async

import kotlinx.coroutines.*

fun main() {
    GlobalScope.launch { // 在後臺啓動一個新協程,並繼續執行以後的代碼
        delay(1000L) // 非阻塞式地延遲一秒
        println("World!") // 延遲結束後打印
    }
    println("Hello,") //主線程繼續執行,不受協程 delay 所影響
    Thread.sleep(2000L) // 主線程阻塞式睡眠2秒,以此來保證JVM存活
}
複製代碼

輸出結果ide

Hello,
World!
複製代碼

本質上,協程能夠稱爲輕量級線程。協程在 CoroutineScope (協程做用域)的上下文中經過 launch、async 等協程構造器(coroutine builder)來啓動。在上面的例子中,在 GlobalScope ,即全局做用域內啓動了一個新的協程,這意味着該協程的生命週期只受整個應用程序的生命週期的限制,即只要整個應用程序還在運行中,只要協程的任務還未結束,該協程就能夠一直運行函數

能夠將以上的協程改寫爲經常使用的 thread 形式,能夠得到相同的結果單元測試

fun main() {
    thread {
        Thread.sleep(1000L)
        println("World!")
    }
    println("Hello,")
    Thread.sleep(2000L)
}
複製代碼

可是若是僅僅是將 GlobalScope.launch 替換爲 thread 的話,編譯器將提示錯誤:

Suspend function 'delay' should be called only from a coroutine or another suspend function
複製代碼

這是因爲 delay() 是一個掛起函數(suspending function),掛起函數只能由協程或者其它掛起函數進行調度。掛起函數不會阻塞線程,而是會將協程掛起,在特定的時候纔再繼續運行

開發者須要明白,協程是運行於線程上的,一個線程能夠運行多個(能夠是幾千上萬個)協程。線程的調度行爲是由 OS 來操縱的,而協程的調度行爲是能夠由開發者來指定並由編譯器來實現的。當協程 A 調用 delay(1000L) 函數來指定延遲1秒後再運行時,協程 A 所在的線程只是會轉而去執行協程 B,等到1秒後再把協程 A 加入到可調度隊列裏。因此說,線程並不會由於協程的延時而阻塞,這樣能夠極大地提升線程的併發靈活度

2、橋接阻塞與非阻塞的世界(Bridging blocking and non-blocking worlds)

在第一個協程程序裏,混用了非阻塞代碼 delay() 與阻塞代碼 Thread.sleep() ,使得咱們很容易就搞混當前程序是不是阻塞的。能夠改用 runBlocking 來明確這種情形

import kotlinx.coroutines.*

fun main() { 
    GlobalScope.launch { // launch a 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
    } 
}
複製代碼

運行結果和第一個程序是同樣的,可是這段代碼只使用了非阻塞延遲。主線程調用了 runBlocking 函數,直到 runBlocking 內部的全部協程執行完成後,以後的代碼纔會繼續執行

能夠將以上代碼用更喜歡的方式來重寫,使用 runBlocking 來包裝 main 函數的執行體:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> { // start main coroutine
    GlobalScope.launch { // launch a new coroutine in background and continue
        delay(1000L)
        println("World!")
    }
    println("Hello,") // main coroutine continues here immediately
    delay(2000L)      // delaying for 2 seconds to keep JVM alive
}
複製代碼

這裏 runBlocking<Unit> { ... } 做爲用於啓動頂層主協程的適配器。咱們顯式地指定它的返回類型 Unit,由於 kotlin 中 main 函數必須返回 Unit 類型,但通常咱們均可以省略類型聲明,由於編譯器能夠自動推導(這須要代碼塊的最後一行代碼語句沒有返回值或者返回值爲 Unit)

這也是爲掛起函數編寫單元測試的一種方法:

class MyTest {
    @Test
    fun testMySuspendingFunction() = runBlocking<Unit> {
        // here we can use suspending functions using any assertion style that we like
    }
}
複製代碼

須要注意的是,runBlocking 代碼塊默認運行於其聲明所在的線程,而 launch 代碼塊默認運行於線程池中,能夠經過打印當前線程名來進行區分

3、等待做業(Waiting for a job)

延遲一段時間來等待另外一個協程運行並非一個好的選擇,能夠顯式(非阻塞的方式)地等待協程執行完成

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    val job = GlobalScope.launch { // launch a new coroutine and keep a reference to its Job
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() // wait until child coroutine completes
//sampleEnd 
}
複製代碼

如今,代碼的運行結果仍然是相同的,可是主協程與後臺做業的持續時間沒有任何關係,這樣好多了

4、結構化併發(Structured concurrency)

以上對於協程的使用還有一些須要改進的地方。GlobalScope.launch 會建立一個頂級協程。儘管它很輕量級,但在運行時仍是會消耗一些內存資源。若是開發者忘記保留對該協程的引用,它將能夠一直運行直到整個應用程序中止。咱們會遇到一些比較麻煩的情形,好比協程中的代碼被掛起(好比錯誤地延遲了太多時間),或者啓動了太多協程致使內存不足。此時咱們須要手動保留對全部已啓動協程的引用以便在須要的時候中止協程,但這很容易出錯

kotlin 提供了更好的解決方案。咱們能夠在代碼中使用結構化併發。正如咱們一般使用線程那樣(線程老是全局的),咱們能夠在特定的範圍內來啓動協程

在上面的示例中,咱們經過 runBlocking 將 main() 函數轉爲協程。每一個協程構造器(包括 runBlocking)都會將 CoroutineScope 的實例添加到其代碼塊的做用域中。咱們能夠在這個做用域中啓動協程,而沒必要顯式地 join,由於外部協程(示例代碼中的 runBlocking)在其做用域中啓動的全部協程完成以前不會結束。所以,咱們能夠簡化示例代碼:

import kotlinx.coroutines.*

fun main() = runBlocking { // this: CoroutineScope
    launch { // launch a new coroutine in the scope of runBlocking
        delay(1000L)
        println("World!")
    }
    println("Hello,")
}
複製代碼

launch 函數是 CoroutineScope 的擴展函數,而 runBlocking 的函數體參數也是被聲明爲 CoroutineScope 的擴展函數,因此 launch 函數就隱式持有了和 runBlocking 相同的協程做用域。此時即便 delay 再久, println("World!") 也必定會被執行

5、做用域構建器(Scope builder)

除了使用官方的幾個協程構建器所提供的協程做用域以外,還可使用 coroutineScope 來聲明本身的做用域。coroutineScope 用於建立一個協程做用域,直到全部啓動的子協程都完成後才結束

runBlocking 和 coroutineScope 看起來很像,由於它們都須要等待其內部全部相同做用域的子協程結束後纔會結束本身。二者的主要區別在於 runBlocking 方法會阻塞當前線程,而 coroutineScope 只是掛起並釋放底層線程以供其它協程使用。因爲這個差異,因此 runBlocking 是一個普通函數,而 coroutineScope 是一個掛起函數

能夠經過如下示例來演示:

import kotlinx.coroutines.*

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

運行結果:

Task from coroutine scope
Task from runBlocking
Task from nested launch
Coroutine scope is over 
複製代碼

注意,在 「Task from coroutine scope」 消息打印以後,在等待 launch 執行完以前 ,將執行並打印「Task from runBlocking」,儘管此時 coroutineScope 還沒有完成

6、提取函數並重構(Extract function refactoring)

抽取 launch 內部的代碼塊爲一個獨立的函數,須要將之聲明爲掛起函數。掛起函數能夠像常規函數同樣在協程中使用,但它們的額外特性是:能夠依次使用其它掛起函數(如 delay 函數)來使協程掛起

import kotlinx.coroutines.*

fun main() = runBlocking {
    launch { doWorld() }
    println("Hello,")
}

// this is your first suspending function
suspend fun doWorld() {
    delay(1000L)
    println("World!")
}
複製代碼

可是若是提取出的函數包含一個在當前做用域中調用的協程構建器的話該怎麼辦? 在這種狀況下,所提取函數上只有 suspend 修飾符是不夠的。爲 CoroutineScope 寫一個擴展函數 doWorld 是其中一種解決方案,但這可能並不是老是適用的,由於它並無使 API 更加清晰。 經常使用的解決方案是要麼顯式將 CoroutineScope 做爲包含該函數的類的一個字段, 要麼當外部類實現了 CoroutineScope 時隱式取得。 做爲最後的手段,可使用 CoroutineScope(coroutineContext),不過這種方法結構上並不安全, 由於你不能再控制該方法執行的做用域。只有私有 API 才能使用這個構建器。

7、協程是輕量級的(Coroutines ARE light-weight)

運行如下代碼:

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(100_000) { // launch a lot of coroutines
        launch {
            delay(1000L)
            print(".")
        }
    }
}
複製代碼

以上代碼啓動了10萬個協程,每一個協程延時一秒後都會打印輸出。若是改用線程來完成的話,很大可能會發生內存不足異常,但用協程來完成的話就能夠輕鬆勝任

8、全局協程相似於守護線程(Global coroutines are like daemon threads)

如下代碼在 GlobalScope 中啓動了一個會長時間運行的協程,它每秒打印兩次 "I'm sleeping" ,而後在延遲一段時間後從 main 函數返回

import kotlinx.coroutines.*

fun main() = runBlocking {
//sampleStart
    GlobalScope.launch {
        repeat(1000) { i ->
            println("I'm sleeping $i ...")
            delay(500L)
        }
    }
    delay(1300L) // just quit after delay
//sampleEnd    
}
複製代碼

你能夠運行代碼並看到它打印了三行後終止運行:

I'm sleeping 0 ... I'm sleeping 1 ...
I'm sleeping 2 ... 複製代碼

這是因爲 launch 函數依附的協程做用域是 GlobalScope,而非 runBlocking 所隱含的做用域。在 GlobalScope 中啓動的協程沒法使進程保持活動狀態,它們就像守護線程(當主線程消亡時,守護線程也將消亡)

相關文章
相關標籤/搜索