iT邦幫忙

2021 iThome 鐵人賽

DAY 28
1
Software Development

Coroutine 停看聽系列 第 28

Day28:複習 Coroutine

不知不覺來到了第28天,最後我們來做個複習吧。

Coroutine 的目的

用來解決非同步程式執行的問題,在以前面對非同步的程式時,我們可能需要建立一個新的執行緒,在這個執行緒上執行完耗時任務之後,接者再把結果由此執行緒傳回至原本的執行緒。

因為非同步程式的特性,我們需要使用 Callback 在耗時任務結束後把結果回傳回來,但是如果有好幾個 Callback 嵌套,也就是一個結果會傳至另一個函式作為參數,這樣就會產生所謂的 Callback hell(回調地獄),增加了維護的難度。

Coroutine 的目的就是要讓非同步的程式執行就像是同步的程式執行一樣,也就是說不需要 Callback 來接收不同執行緒的結果。


四大特性

  1. 編寫非同步程式與同步程式一樣容易。
    • Callback hell 再會了。
    • 從哪裡暫停,就從哪裡恢復。
  2. Coroutines 可看作是輕量級的執行緒。
    • Coroutine 是在執行緒底下,每一個執行緒中含有很多個 Coroutine。
  3. 可以輕易地在執行緒中切換。
    • 可以使用 Dispatchers 使用不同的執行緒。
    • Coroutine 定義了幾種 Dispatchers ,我們只需要選擇適當的 Dispatchers,然後 Coroutine 就會幫我們選擇適當的執行緒。
  4. 輕鬆取消 Coroutine 的執行。
    • 若要結束執行緒,則必須讓執行緒裏面的任務執行完畢之後,才可以結束,否則有可能會造成 memory leak。
    • 但是,Coroutine 能夠輕鬆被取消,每一個 Coroutine 都會回傳一個 Job,其中包含了 cancel() 只要呼叫 cancel() 該 Coroutine 的任務就會被取消。
    • 因為 Coroutine 是協作式的多工,執行中的程式會定期地放棄自己的執行權利,那麼當我們呼叫 cancel() 的時候,我們就可以自行將任務給取消。

三大要素

Suspend 函式

我們將耗時任務寫在 suspend 函式中,當程式執行到 suspend 函式時,將會暫停該 coroutine ,並且切換自動切換至其他的 coroutine,等到 suspend 函式完成它的任務時,就會在原本暫停的點繼續開始下面的任務。

suspend 函式只能在 coroutine scope 或是另外的 suspend 函式內呼叫。

CoroutineScope (範圍)

要執行 suspend 函式,必須要在 CoroutineScope 中執行,而建立 Scope 的方式有兩種,一種是 launch ,另一種則是 async 。其中 launch 是沒有回傳值的(Side-effect),而 async 是有回傳值的。launch 回傳的是 Job,而 async 回傳的是 Deferred,其中 Deferred 也是繼承 Job 的類別。如前面所說,我們可以直接呼叫 Job 的 cancel() 取消 coroutine 的任務。

Dispatchers (調度器)

建立執行緒是需要較多資源,如果每次需要耗時任務都建立一個執行序,那麼是不太切實際的,所以Coroutine 中,使用者是不能直接使用執行緒的,這邊提供了幾種不同的 Dispatchers,使用者根據需求選擇不同的 Dispatchers,Coroutine 就會選擇適當的執行緒/執行緒池。這些執行緒/執行緒池是不會被關閉的,所以任務可以很快速的切換至不同的執行緒中。另外,如果要在 Coroutine 中切換不同的調度器,我們可以使用 withContext


結構化併發

我們可以在一個 Coroutine Scope 中執行多個 Job,它們彼此之間的關係是父-子的關係,也就是說,當 Coroutine Scope 裏面的所有 Job 結束之後,外層(父層)的 Coroutine 才能夠結束。如果其中一個子 Coroutine 發生錯誤被取消,那麼後面還沒有完成的任務也會一併被取消。另外,如果父 coroutine 被取消,那麼所有在裏面的子 Coroutine 也會一併被取消。這樣子的好處就是不會有父類別已經被取消,但是子 coroutine 卻還在執行,這樣子就有可能會發生 memory leak 的情況。

SupervisorJob()

如果在一個 CoroutineScope 中,其中一個子 coroutine 被取消,如果我們使用的是 Job(),那麼後面的所有 job 都會被取消。但是如果使用的是 SupervisorJob(),當其中一個 Job 發生錯誤被取消時,後面的任務還是會繼續執行直到完成。


例外處理

在 coroutine 中,我們一樣可以使用 try-catch 來將可能會發生例外(Exception) 的程式碼包起來,當發生例外的時候,就會被 try-catch 給攔截下來。如果我們希望能夠在 CoroutineScope 的範圍內能夠捕捉例外呢?我們可以使用 CoroutineExceptionHandler。不過 async 的例外會在呼叫 await() 時才會發生,所以如果要捕捉 async 的例外,則還是必須要在 await() 上使用 try-catch 。


內建的 suspend 函式

  • delay(): 將目前的 coroutine 暫停 n ms。
  • yield():將目前 coroutine 的執行緒使用權讓給下一個。
  • withContext:在目前的 coroutine 中使用另外的 CoroutineContext。
  • withTimeout:使用 withTimeout 在耗時任務上,當任務的執行時間超過預期,則會報出TimeoutCancellationException。
  • withTimeoutOrNull:與 withTimeout 類似,不過時間到的時候是會回傳 null 而不是報出 TimeoutCancellationException。
  • joinAll():如果有多個 Job 存在 List 中,我們可以使用 joinAll() 讓所有的 Job 都呼叫其 join(),其中呼叫 join() 是會讓該 coroutine 的任務提前先做。
  • awaitAll():如果有多個 Deferred 存在 List 中,我們可以使用 awaitAll() 來將所有的任務都被呼叫 await() ,所以 awaitAll() 得到結果就是所有的 async 都完成的時候。
  • cancelAndJoin():如果 job 裏面的任務太久,我們想要取消它,我們會使用 cancel(),當取消之後,我們可以在用 join() 執行該任務後面的動作。在 Coroutine 中,提供了一個整合的函式, cancelAndJoin() 讓我們可以一次呼叫兩個函式。

呼,前十四天的內容就在這篇做個複習了,如果有什麼遺漏,或是有錯誤,麻煩請跟我說,下一篇將複習後面的部分: Channel、Flow、SharedFlow、StateFlow。

特別感謝

Kotlin Taiwan User Group

Kotlin 讀書會


上一篇
Day27:測試 Coroutine
下一篇
Day29:複習 Channel、Flow
系列文
Coroutine 停看聽30

尚未有邦友留言

立即登入留言