上文我們已經知道了,在沒有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的生命週期如下圖所示:
處於不同生命週期時的不同狀態位:
上面我們提到過取消子協程的任務只需調用 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 的操作。
那麼,面對代碼塊中沒有調用這些掛起函數的情況,我們怎麼才能讓我們的子協程擁有可被取消的能力呢?
- 定期調用 kotlinx.coroutines 中的掛起函數,如 yield
- 顯示檢查協程的取消狀態
方式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 函數。