1. 程式人生 > 其它 >Android Kotlin 協程async

Android Kotlin 協程async

measureTimeMillis 統計一段程式碼耗時
內斂函式 measureTimeMillis{ } 可以很方便的統計一段程式碼執行的耗時。

使用:

GlobalScope.launch {
    val time = measureTimeMillis {
        delay(1000)
        Log.d("zyj-", "日誌")
    }
    Log.d("zyj-", "耗時:$time")
}

輸出結果:

D/zyj-: 日誌
D/zyj-: 耗時:1010

使用預設順序
定義兩個耗時函式:

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // 假設我們在這裡做了一些有用的事
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // 假設我們在這裡也做了一些有用的事
    return 29
}

使用預設的順序呼叫:

val time = measureTimeMillis {
    val one = doSomethingUsefulOne()
    val two = doSomethingUsefulTwo()
    println("The answer is ${one + two}")
}
println("Completed in $time ms")

它的列印輸出如下:

The answer is 42
Completed in 2017 ms

從輸出結果上看,兩個耗時任務是序列的,總耗時= 耗時函式1 + 耗時函式2

使用 async 併發
如果 doSomethingUsefulOne 與 doSomethingUsefulTwo 之間沒有依賴,並且我們想更快的得到結果,讓它們進行 併發 嗎?這就是async 可以幫助我們的地方。

在概念上,async 就類似於 launch。它啟動了一個單獨的協程與其它所有的協程一起併發的工作。不同之處在於 launch 返回一個 Job 並且不附帶任何結果值,而 async 返回一個 Deferred—— 一個非阻塞 future, 這代表了一個將會在稍後提供結果的 promise。你可以使用 .await()在一個延期的值上得到它的最終結果, 但是 Deferred 也是一個 Job,所以如果需要的話,你可以取消它。

val time = measureTimeMillis {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

它的列印輸出如下:

The answer is 42
Completed in 1017 ms

這裡快了兩倍,因為兩個協程併發執行。 請注意,使用協程進行併發總是顯式的。

惰性啟動的 async
可選的,async 可以通過將 start 引數設定為 CoroutineStart.LAZY 而變為惰性的。 在這個模式下,只有結果通過 await 獲取的時候協程才會啟動,或者在 Job 的 start函式呼叫的時候。執行下面的示例:

val time = measureTimeMillis {
    val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() }
    val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() }
    // 執行一些計算
    one.start() // 啟動第一個
    two.start() // 啟動第二個
    println("The answer is ${one.await() + two.await()}")
}
println("Completed in $time ms")

它的列印輸出如下:

The answer is 42
Completed in 1017 ms

因此,在先前的例子中這裡定義的兩個協程沒有執行,但是控制權在於程式設計師準確的在開始執行時呼叫 start。我們首先 呼叫 one,然後呼叫 two,接下來等待這個協程執行完畢。

注意,如果我們只是在 println 中呼叫 await,而沒有在單獨的協程中呼叫 start,這將會導致順序行為,直到 await 啟動該協程 執行並等待至它結束,這並不是惰性的預期用例。 在計算一個值涉及掛起函式時,這個 async(start = CoroutineStart.LAZY)的用例用於替代標準庫中的 lazy 函式。

構建async 風格的函式
我們可以定義非同步風格的函式來 非同步 的呼叫 doSomethingUsefulOne 和 doSomethingUsefulTwo 並使用 async 協程建造器並帶有一個顯式的 GlobalScope 引用。 我們給這樣的函式的名稱中加上“……Async”字尾來突出表明:事實上,它們只做非同步計算並且需要使用延期的值來獲得結果。

// somethingUsefulOneAsync 函式的返回值型別是 Deferred<Int>
fun somethingUsefulOneAsync() = GlobalScope.async {
    doSomethingUsefulOne()
}

// somethingUsefulTwoAsync 函式的返回值型別是 Deferred<Int>
fun somethingUsefulTwoAsync() = GlobalScope.async {
    doSomethingUsefulTwo()
}

使用

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 我們可以在協程外面啟動非同步執行
        val one = somethingUsefulOneAsync()
        val two = somethingUsefulTwoAsync()

        // 但是等待結果必須呼叫其它的掛起或者阻塞
        // 當我們等待結果的時候,這裡我們使用 `GlobalScope.launch{ }` 來阻塞主執行緒
        GlobalScope.launch {
            println("The- answer is ${one.await()}  ${two.await()}")
        }
    }
}

這種帶有非同步函式的程式設計風格僅供參考,因為這在其它程式語言中是一種受歡迎的風格。在 Kotlin 的協程中使用這種風格是強烈不推薦的, 原因如下所述。

考慮一下如果 val one = somethingUsefulOneAsync() 這一行和 one.await() 表示式這裡在程式碼中有邏輯錯誤, 並且程式丟擲了異常以及程式在操作的過程中中止,將會發生什麼。 通常情況下,一個全域性的異常處理者會捕獲這個異常,將異常列印成日記並報告給開發者,但是反之該程式將會繼續執行其它操作。但是這裡我們的 somethingUsefulOneAsync仍然在後臺執行, 儘管如此,啟動它的那次操作也會被終止。這個程式將不會進行結構化併發,如下一小節所示。

使用 async 的結構化併發
讓我們使用使用 async 的併發這一小節的例子並且提取出一個函式併發的呼叫 doSomethingUsefulOne 與 doSomethingUsefulTwo 並且返回它們兩個的結果之和。 由於 async 被定義為了 CoroutineScope上的擴充套件,我們需要將它寫在作用域內,並且這是 coroutineScope 函式所提供的:

suspend fun concurrentSum(): Int = coroutineScope {
    val one = async { doSomethingUsefulOne() }
    val two = async { doSomethingUsefulTwo() }
    one.await() + two.await()
}

這種情況下,如果在 concurrentSum 函式內部發生了錯誤,並且它丟擲了一個異常, 所有在作用域中啟動的協程都會被取消。

val time = measureTimeMillis {
    println("The answer is ${concurrentSum()}")
}
println("Completed in $time ms")

從上面的 main 函式的輸出可以看出,我們仍然可以同時執行這兩個操作:

The answer is 42
Completed in 1017 ms

取消始終通過協程的層次結構來進行傳遞:

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    try {
        failedConcurrentSum()
    } catch(e: ArithmeticException) {
        println("Computation failed with ArithmeticException")
    }
}

suspend fun failedConcurrentSum(): Int = coroutineScope {
    val one = async<Int> { 
        try {
            delay(Long.MAX_VALUE) // 模擬一個長時間的運算
            42
        } finally {
            println("First child was cancelled")
        }
    }
    val two = async<Int> { 
        println("Second child throws an exception")
        throw ArithmeticException()
    }
    one.await() + two.await()
}

請注意,如果其中一個子協程(即 two)失敗,第一個 async 以及等待中的父協程都會被取消:

Second child throws an exception
First child was cancelled
Computation failed with ArithmeticException