在進入 Coroutine(協程)的世界之前,先把「程式為什麼需要非同步」這件事釐清。Kotlin Coroutine 的設計初衷,正是要讓我們在不犧牲可讀性的前提下,處理好這些長時間或不確定何時完成的工作。

程式的兩種執行形態#

按照程式碼的執行順序,可以分成兩類:

  • 同步(Synchronous):依照原始碼的順序逐行執行,前一段沒結束,後一段就不會開始。
  • 非同步(Asynchronous):呼叫端不必等待結果回來,可以先去做別的事,等結果就緒再回頭處理。

同步式的呼叫總時間,等於每一段函式所花時間的總和。三段串接的同步呼叫像田徑場上的接力賽,一棒接著一棒。

fun sayHello() {
    println("Hello")
    println("Nice to meet you!")
}

呼叫 sayHello() 後得到的輸出,會剛好對應原始碼的順序:

Hello
Nice to meet you!

為什麼需要非同步#

把場景換成「登入後抓資料、再把資料畫到畫面上」就能看到同步的不足。三個步驟分別是:

  1. 登入服務並換取 Token。
  2. 透過 Token 取得最新內容。
  3. 把內容繪製到畫面上。
fun showContents() {
    val token = login(userName, password)
    val contents = fetchLatestContents(token)
    showContents(contents)
}

如果這些函式都直接在主執行緒(Main Thread)上呼叫,遇到 fetchLatestContents() 需要連線到伺服器這類耗時動作,畫面就會被卡住。Android 上若主執行緒卡住超過五秒,系統會直接彈出 ANR(Application Not Responding)的提示。

非同步的價值就在這裡:請別人去做事,做完再通知自己,不要讓主執行緒被綁住。

阻塞(Blocking)的意義#

「阻塞」指的是某段程式碼在等待某件事完成之前,會把所在的執行緒鎖住,讓這條執行緒沒辦法去做其他工作。同步式呼叫如果搭配長時間的 I/O,就會發生阻塞;非同步式呼叫則是把等待這段時間還給執行緒,讓它可以處理別的任務。

同步不等於阻塞。同步只是描述「依序執行」這個語意,阻塞則是描述「執行緒在等待時被鎖住」。但在大多數同步式 I/O 的情境下,兩者經常一起出現。

非同步要解決的問題#

從上面的對照可以歸納非同步要處理的兩種情況:

  • 耗時任務:例如網路請求、檔案 I/O、複雜運算,不能讓主執行緒一直空轉等待。
  • 延遲任務:呼叫的當下不用立刻執行,而是等一段時間後再恢復。

Kotlin 的 Coroutine 之所以被設計出來,就是為了用更直覺的寫法處理這兩種需求,並且在需要時能輕鬆取消還沒完成的工作。

小結#

  • 同步的程式以原始碼順序執行,直覺但容易在耗時工作上卡住執行緒。
  • 非同步的程式不依靠原始碼順序,可以把工作交給別的執行緒,主執行緒就能去做別的事。
  • 阻塞描述的是執行緒被等待綁住的狀態,是同步寫法常見的副作用。
  • Coroutine 的目標,是讓非同步程式可以用接近同步的方式撰寫,同時保留隨時取消的能力。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10259764