Kotlin協程 —— 今天說說 launch 與 async

上文我們已經知道了,在沒有CoroutineScope時,我們可以通過實現該接口,或者使用 runBlocking 方法,來使我們的程序可以調用 suspend 掛起函數。

今天我們來看看 Builders.common 下的幾個構建協程函數:launch 與 async 函數

launch 函數

在上一篇文章中我們已經接觸過數次 launch 函數了,他的主要作用就是在當前協程作用域中創建一個新的協程。在子協程中執行耗時任務或掛起函數時,只對子協程有影響,上文中我們提到過的這是 CoroutineScope 的原因。

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

launch 函數的返回值是 Job,比如說當用戶關閉頁面時,後臺請求尚未返回,但此時結果已經無關緊要了,我們可以通過Job.cancel() 函數來取消掉當前執行的協程任務。

再舉個栗子🌰,如果我們將一些耗時任務放在子協程中處理,但是父協程需要用到子協程的結果,這時候我們該怎麼辦?這就是我們要介紹的 Job.join() 函數

public suspend fun join()

該函數將會掛起當前的協程直到 Job 的狀態變爲isCompleted ,在父線程中調用了 join 之後,將會在調用出掛起協程,直到子協程執行完成(或是取消)。

    @Test
    fun testjob()= runBlocking {
        var string = "4321"
        val job = launch {
           delay(3000)
           string = "1234"
        }
        println(string)
        job.join()
        println(string)
    }

上述代碼的執行結果爲:

4321
1234

這是因爲第一次執行打印時job還沒有執行完畢,所以 string 的值爲初始值,我們調用 join 將主協程掛起之後,主協程將會一直阻塞到 launch 內的代碼執行完畢,再次打印就是重新賦值後的新值。

如果我們爲 launch函數設置 CoroutineStart 參數 爲 LAZY 時,join() 函數還起到啓動子協程的作用。

Job的生命週期如下圖所示:
Job生命週期
處於不同生命週期時的不同狀態位:
Job的狀態
上面我們提到過取消子協程的任務只需調用 cancel 函數即可,但是這存在一個隱患,即子協程有可能在取消的過程中改變了父協程的變量狀態,因此爭取的取消應該是這樣的:

job.cancel()
job.join()

即調用取消函數後立刻在父協程掛起,直到取消成功,再繼續執行,官方提供了簡化方法 job.cancelAndJoin()

Job爲什麼可以被取消?

@Test
fun test1()= runBlocking {
    val job = launch {
        repeat(10) {
            delay(500L)
            println(it)
        }
    }
    delay(1000L)
    job.cancelAndJoin()
}

上述代碼執行到 cancelAndJoin() 函數時,子協程的任務將會終止。但是如果我們將delay() 函數替換成 Thread.sleep() 這時你會發現,子協程沒有被取消,這是因爲什麼呢?如果對上述代碼 delay 函數進行try catch,你會發現在調用cancel函數後,delay 函數拋出了一個JobCancellationException異常。

在文檔中有這樣一句話,不是很好理解:

協程的取消是 協作 的。一段協程代碼必須協作才能被取消。

這句話說白了就是整個子協程中的代碼必須要是可以被取消的(所有 kotlinx.coroutines 中的掛起函數都是 可被取消的 ),這些掛起函數會檢查協程的取消, 並在取消時拋出 CancellationException,從而達成取消 Job 的操作。

那麼,面對代碼塊中沒有調用這些掛起函數的情況,我們怎麼才能讓我們的子協程擁有可被取消的能力呢?

  1. 定期調用 kotlinx.coroutines 中的掛起函數,如 yield
  2. 顯示檢查協程的取消狀態

方式2的代碼如下:

@Test
fun test2()= runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch(Dispatchers.Default) {
        var nextPrintTime = startTime
        var i = 0
        while (isActive) { // 可以被取消的計算循環
            // 每秒打印消息兩次
            if (System.currentTimeMillis() >= nextPrintTime) {
                println("job: I'm sleeping ${i++} ...")
                nextPrintTime += 500L
            }
        }
    }
    delay(1000L)
    job.cancelAndJoin()
}

要注意的一點是,這裏必須爲launch函數指定 CoroutineContext,且不能爲 Dispatchers.Unconfined 。

資源釋放

那麼如果我們需要在協程任務取消時,釋放一些資源應該如何處理(比如輸入輸出流的關閉等)?這裏我們可以使用 try{....} finally{.....} 表達式來處理,或者使用 use 函數。

use函數是 kotlin 的一個高階擴展函數,凡是實現了Closeable 的對象都可以使用 use 函數,從而省去異常後的資源釋放。可以參考閱讀:https://blog.csdn.net/qq_33215972/article/details/79762878

運行不可取消的代碼塊

如果我們在釋放資源後仍需要調用部分掛起函數應該怎麼辦呢?很簡單,只需要調用 withContext(NonCancellable) {……} 來運行不可取消的代碼即可。

需要注意,不同於 launch 函數與 async 函數,withContext 函數是一個掛起函數,也就是說他只能在一個協程中調用,並且還會掛起當前調用的協程,直至其內部代碼運行完畢,所以一般 withContext 函數在協程內部被用於切換不同線程,如執行耗時任務完畢得到返回值後,切換到 UI 線程,將數據顯示到 View 上。

async 函數

老規矩,先看源碼:

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T> {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyDeferredCoroutine(newContext, block) else
        DeferredCoroutine<T>(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

與 launch 幾乎完全相同,同樣是CoroutineScope的一個擴展函數,用於開啓一個新的子協程,與 launch 函數一樣可以設置啓動模式,不同的是它的返回值爲 Deferred。簡單理解的話,這就是一個帶返回值的 launch 函數!

Deferred 繼承自 Job 接口,但是擴展了幾個函數,用於獲取 async 函數的返回值。

1、await() 函數
這是一個掛起函數,返回值爲 Deferred,T 爲協程的返回值。
使用方法如下:

@Test
fun testAsync()= runBlocking {
    val deferred = async(Dispatchers.IO) {
        //此處是一個耗時任務
        delay(3000L)
        "ok"
    }
    //此處繼續執行其他任務
    //..........
    val result = deferred.await()  //此處獲取耗時任務的結果,我們掛起當前協程,並等待結果
    withContext(Dispatchers.Main){
        //掛起協程切換至UI線程 展示結果
        println(result)
    }
}

取消線程的方式與 Job 是一致的。

2、getCompleted() 函數
這是一個普通函數,用於獲取協程返回值,沒有 Deferred 進行包裝。如果協程任務還沒有執行完成則會拋出 IllegalStateException ,如果任務被取消了也會拋出對應的異常。所以在執行這個函數之前,可以通過 isCompleted 來判斷一下當前任務是否執行完畢了。

3、getCompletionExceptionOrNull()
getCompletionExceptionOrNull() 函數用來獲取已完成狀態的Coroutine異常信息,如果任務正常執行完成了,則不存在異常信息,返回null。如果還沒有處於已完成狀態,則調用該函數同樣會拋出 IllegalStateException。

總結:

launch 與 async 這兩個函數大同小異,都是用來在一個 CoroutineScope 內開啓新的子協程的。不同點從函數名也能看出來,launch 更多是用來發起一個無需結果的耗時任務(如批量文件刪除、創建),這個工作不需要返回結果。async 函數則是更進一步,用於異步執行耗時任務,並且需要返回值(如網絡請求、數據庫讀寫、文件讀寫),在執行完畢通過 await() 函數獲取返回值。

如何選擇這兩個函數就看我們自己的需求啦,比如只是需要切換協程執行耗時任務,就用 launch 函數。如果想把原來的回調式的異步任務用協程的方式實現,就用 async 函數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章