suspend fun<T> withContext(context: CoroutineContext,
block: suspend CoroutineScope.()->T):T
withContext
是用來在現有的 coroutine 中,使用新的 CoroutineContext 來建立一個執行區塊。我們最常看到的是拿來做更新畫面使用,也就是說當 coroutine 執行的任務完成之後,我們可以使用 withContext(Dispatchers.Main)
將執行緒切回主執行緒並更新。
這邊我們看一個範例:
fun main() = runBlocking {
val job = launch {
println("inside: ${Thread.currentThread().name}")
withContext(Dispatchers.Default) {
println("Default dispatcher: ${Thread.currentThread().name}")
}
}
println("outer: ${Thread.currentThread().name}")
}
outer: main
inside: main
Default dispatcher: DefaultDispatcher-worker-1
→ 根據我們前面所說的巢狀架構,這段程式碼會先列印最外側的 outer: main
,接著才會是執行 launch
所建立出來的 coroutine,並依序執行 coroutine 裏面的程式。
→ 在 launch
中,我們使用 withContext(Dispatchers.Default)
,也就是說在這個區塊中我們將使用 Dispatchers.Default,而從 log 看來的確也已經切換到 DefaultDispatcher。
withContext 所建立出來的區間是可以被取消的,當調用 withContext 的 coroutine 被取消之後,withContext 也就會被取消。如下例:
fun main() = runBlocking {
val job = launch {
println("inside: ${Thread.currentThread().name}")
withContext(Dispatchers.Default) {
delay(200)
println("Default: ${Thread.currentThread().name}")
}
}
delay(100)
job.cancel()
job.join()
println("outer: ${Thread.currentThread().name}")
}
inside: main
outer: main
→ 外側的 coroutine 因為呼叫了 delay()
之後,所以把外側的 coroutine 切換至等待狀態,並尋找下一個可執行的 coroutine。那麼這邊我們看到列印出 inside: main
,緊接者進入 withContext(Dispatchers.Default)
,在 withContext 中立刻就呼叫一個 delay() ,所以此 coroutine 又被切換成等待狀態,並且找尋下一個可執行的 coroutine。此時,外側的 coroutine 已經結束它的延時,所以呼叫到了 job.cancel()
,這個時候因為 launch
裏面尚有任務還在等待,所以就直接被取消。最後則是列印出 outer: main
。
在前面的範例中,我們知道 withContext 的區塊會隨著啟動它的 coroutine 被取消也跟著被取消,what if 我們不希望讓 withContext 被取消呢?
我們可以在 withContext 的 CoroutineContext 中加上 NonCancellable
,加上 NonCancellable 之後,被 withContext 包住的區塊就不會因為外側的 coroutine 取消而跟著被取消。
如下面的範例:
fun main() = runBlocking {
val job = launch {
println("inside: ${Thread.currentThread().name}")
withContext(Dispatchers.Default + NonCancellable) {
delay(200)
println("Default: ${Thread.currentThread().name}")
}
}
delay(100)
job.cancel()
job.join()
println("outer: ${Thread.currentThread().name}")
}
→ 這個範例跟前面一個幾乎相同,唯一不同的地方在於我加上 NonCancellable
在 withContext 的 CoroutineContext 中,結果會是如何呢?我們看一下:
inside: main
Default: DefaultDispatcher-worker-1
outer: main
雖然 job 的 cancel()
被呼叫,但是 withContext 裏面的區塊仍然會執行。
在最前面有說,我們經常使用 withContext 在更新畫面上,也就是說,我們可以讓更新畫面這段程式碼無論如何都會執行,而不會因為外側的 coroutine 被取消而跟著被取消。
withTimeout
顧名思義就是跟 timeout 有關係,我們先看他是怎麼使用的:
suspend fun <T> withTimeout(timeMillis: Long, block: suspend CoroutineScope.() -> T): T
→ 它有包含兩個參數,一個是時間,另一個是 CoroutineScope 也就是執行的區塊。
要如何使用呢?我們看一下下面的範例:
fun main() = runBlocking {
val job = launch() {
println("inside: ${Thread.currentThread().name}")
withTimeout(200) {
repeat(10) {
println("delay $it times")
delay(50)
yield()
}
println("withTimeout")
}
}
delay(100)
println("outer: ${Thread.currentThread().name}")
}
→ 在上面這個範例中,同樣的我們在 runBlocking
裏面使用了 launch
建立了一個 coroutine,而在 launch 的下方則是有幾行程式碼。
→ 首先,會先執行最外側的 delay(100)
,這時最外側的 Coroutine 就被設定為暫停,此時會尋找下一個適當的 coroutine 來執行。在這個範例中,適當的 coroutine 是 launch 的區塊。launch 內部有一重複 10 次,在每一次執行的時候,都會將 coroutine 暫停 50 毫秒,在暫停完成之後,會把執行緒使用權切換至外側,但是外側的 coroutine 還在等待,所以使用權又切回來執行下一次。如果要完成重複十次的任務,需要花費 10*50 = 500 毫秒。
→ 假設我們希望執行的時間能夠在 200 毫秒,在這個 repeat(10){ ... }
的外側,我們加上了 withTimeout(200)
,當 200 毫秒結束之後,我們就會取消這個區塊裏面的所有任務。所以這段程式碼的輸出會是:
inside: main
delay 0 times
delay 1 times
outer: main
delay 2 times
delay 3 times
→ 可以發現, repeat 區塊的程式只有跑了四次,符合我們的需求。
其實當 withTimeout 的時間到了之後,是會拋出 TimeoutCancellationException
,只不過由於 TimeoutCancellationException 是 CancellationException
的子類別,所以這個例外會被 coroutine 給吃掉。
但是,如果我們真的需要處理這個例外,我們可以使用 try-catch
來攔截 TimeoutCancellationException。如下:
fun main() = runBlocking {
val job = launch() {
println("inside: ${Thread.currentThread().name}")
try {
withTimeout(200) {
repeat(10) {
println("delay $it times")
delay(50)
yield()
}
println("withTimeout")
}
} catch (e: TimeoutCancellationException) {
println(e.message)
}
}
delay(100)
println("outer: ${Thread.currentThread().name}")
}
inside: main
delay 0 times
delay 1 times
outer: main
delay 2 times
delay 3 times
Timed out waiting for 200 ms
將上例的 withTimeout 用 try-catch
包起來之後,我們就可以接收到這個例外了。
如同 withContext,withTimeout 也同樣是可以取消的。如下例:
fun main() = runBlocking {
val job = launch() {
println("inside: ${Thread.currentThread().name}")
try {
withTimeout(200) {
repeat(10) {
println("delay $it times")
delay(50)
yield()
}
println("withTimeout")
}
} catch (e: TimeoutCancellationException) {
println(e.message)
}
}
delay(100)
job.cancel()
job.join()
println("outer: ${Thread.currentThread().name}")
}
→ 我們在外側 coroutine delay(100)
之後加上了 job.cancel()
,所以當外側 coroutine 的暫停時間結束後,就會把內側的 coroutine 給取消掉。哪麼這個結果會是如何呢?
inside: main
delay 0 times
delay 1 times
outer: main
→ 你應該有猜到,因為 withTimeout 是可以取消的,所以當外側取消了內側的 coroutine ,那麼連帶 withTimeout 也一並被取消了,所以 repeat 第三次、第四次就被取消沒做了。
跟 withTimeout
非常的像,只不過一個是會拋出 TimeoutCancellationException,而另一個是回傳 null,我們看下面的範例:
fun main() = runBlocking {
val job = launch() {
println("inside: ${Thread.currentThread().name}")
val timeout = withTimeoutOrNull(600) {
repeat(10) {
println("delay $it times")
delay(50)
yield()
}
return@withTimeoutOrNull 10
}
println("timeout= $timeout")
}
delay(100)
println("outer: ${Thread.currentThread().name}")
}
→ 跟上面的範例很相像,差異只在於我們在 withTimeoutOrNull(){...}
的最後加上了一個回傳值,所以當程式碼順利在時限內完成,就會回傳這個值,否則就會回傳 null
。
→ 如果 timeout 是 600 毫秒,那這段程式碼可以正常跑完,所以結果會是
inside: main
delay 0 times
delay 1 times
outer: main
delay 2 times
delay 3 times
delay 4 times
delay 5 times
delay 6 times
delay 7 times
delay 8 times
delay 9 times
timeout= 10
將 timeout 改成 200 之後,這段程式就沒有辦法執行 10 次,最多就只能跑 4 次,我們看看結果會是如何?
fun main() = runBlocking {
val job = launch() {
println("inside: ${Thread.currentThread().name}")
val timeout = withTimeoutOrNull(200) {
repeat(10) {
println("delay $it times")
delay(50)
yield()
}
return@withTimeoutOrNull 10
}
println("timeout= $timeout")
}
delay(100)
println("outer: ${Thread.currentThread().name}")
}
inside: main
delay 0 times
delay 1 times
outer: main
delay 2 times
delay 3 times
timeout= null
→ 原本輸出應該是 10,但是因為 timeout 的關係,所以只能完成四次,所以最後 withTimeoutOrNull
會輸出 null
。
本篇文章介紹的三個 suspend 函式, withContext、withTimeout、withTimeoutOrNull。
withContext 適合使用在執行完非同步的呼叫後,需要切換成主執行緒並更新畫面的情境,而另外兩個與 timeout 有關的函式,主要就是要看使用者的需求,如果超時之後就不管他,可以使用 withTimeout,如果執行的區塊是有一個回傳值,withTimeoutOrNull 或許就比較適合了。
有興趣的讀者歡迎參考:https://coroutine.kotlin.tips/
天瓏書局