iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
Software Development

異步程式設計修煉:Kotlin Coroutines 完全指南系列 第 3

Day 02:第一個 suspend function,暫停到底暫停了什麼

  • 分享至 

  • xImage
  •  

Day 01:為什麼你需要協程,從一個會卡住主執行緒的下載函式說起》 停在一個問題上:如果一段程式碼能看起來依序寫下去,遇到等待卻不會卡住主執行緒,那麼它在等待的那個當下到底發生了什麼事,是被誰暫停的,又是怎麼知道該從哪裡接著往下走。今天要先從語法表面回答第一層:Kotlin 用什麼方式,讓一個函式擁有「可以暫停」這種能力。

一個函式要怎麼暫停後又接回來

一個函式跑到一半,要暫停下來讓出資源,之後又能準確接回原本的位置繼續執行,這件事聽起來有點反直覺。一般函式一旦開始執行,就是從第一行跑到最後一行,中途沒有「先擱著,等一下再回來」這種選項。

Kotlin 的做法,是在函式前面加上一個修飾字:suspend。被這個修飾字標記過的函式,具備一種一般函式沒有的能力,可以在執行期間暫停並讓出執行緒,之後再恢復執行,而且這整個過程不會阻塞底層的執行緒。這種函式,這個系列會固定稱呼它為 suspend function,全系列後續一律使用這個詞,不另外翻譯成中文,你會在接下來每一篇文章裡持續看到它。

如果你之前接觸過 JavaScript 的 async/await 或 C# 的類似語法,這裡的 suspend 在概念定位上確實有些相似之處,都是在標記「這個函式內部可能會發生等待」。但兩者的對應關係不能直接畫上等號,後面幾段會慢慢說明差異藏在哪裡,這裡先記住一件事就好:suspend 是一個標記,不是一個會自動生效的魔法。

suspend 不是魔法關鍵字

初次看到 suspend 這個修飾字,很容易產生一種直覺:只要把它加在函式前面,這個函式的內容就會自動變成在背景執行緒運作,函式呼叫也會自動變快。這個直覺並不成立。

suspend 修飾字真正做的事情,是賦予這個函式一個資格:可以呼叫其他 suspend function,並且在呼叫過程中安全地暫停。它的作用更接近一份合約,告訴呼叫者與編譯器,這個函式內部可能會發生暫停,因此它只能從另一個 suspend function 裡被呼叫,本身並不會自動觸發任何效果。這個限制是協程設計上刻意訂出的規則,用來確保暫停行為只會發生在明確承諾過的呼叫鏈上。

這裡有一個常見的初學誤區,值得先點出來。假設你把一個原本耗時的操作直接標記成 suspend,但函式內部完全沒有呼叫任何真正具備暫停能力的操作,這種函式依然可以通過編譯。它看起來像一個 suspend function,簽名上有那個修飾字,可是實際執行的時候,它從頭到尾都是完全同步阻塞的,只是多了一個修飾字而已,行為跟一般函式沒有任何差別。這個現象背後的原因,牽涉到編譯器實際上對 suspend function 做了什麼事,這部分留到下一篇再拆解,今天只需要先記住這個現象本身:加上 suspend 這個動作,本身不會讓任何事情自動變成非阻塞。

暫停不等於阻塞,這是今天最重要的一句話

搞清楚 suspend 不是魔法之後,接下來要建立今天最核心的一組對照:暫停,跟阻塞,是兩件不同的事。

阻塞指的是,呼叫端的執行緒被完全佔用,什麼事都做不了,只能死等結果回來。《Day 01:為什麼你需要協程,從一個會卡住主執行緒的下載函式說起》那個依序下載圖片、等待伺服器回應時畫面整個凍結的情境,正是阻塞最典型的樣貌。

暫停指的是另一種行為。協程執行到某個點時,可以先把「目前執行到哪裡、接下來該做什麼」這件事記下來,然後把底層的執行緒讓出去,讓這條執行緒可以先去做別的事。等結果回來以後,協程再從剛才記下的地方接續執行。整個過程中,執行緒沒有被單一協程死死綁住,它是可以被暫時借走、再還回來的資源。

拿示範情境具體對照一次。如果下載圖片的函式是一個 suspend function,呼叫它去下載某張圖片時,程式碼在等待網路回應的這段期間可以暫停,並且讓出執行緒,讓同一條執行緒有機會先去處理其他工作。等圖片真正下載完成,協程再回來繼續處理這張圖片的後續邏輯,比如把資料交給下一步驟。同樣是「等網路回應」,阻塞讓執行緒被迫閒置死等,暫停則讓執行緒可以先去做別的事,等時機到了再回頭接手。

今天先只建立這個方向性的差異,執行緒具體是怎麼被讓出、又是怎麼被找回來接續執行,這些底層細節留到下一篇再展開。

寫一個最小的 suspend function 長什麼樣子

概念談了不少,該看一眼實際的語法長相了。好消息是,宣告一個 suspend function 的語法非常輕量,只需要在函式前面加上 suspend 這個修飾字,函式簽名的其他部分跟一般函式沒有任何差異。真正的複雜度,藏在函式內部呼叫的行為與底層機制裡,跟宣告語法本身沒有關係。

拿《Day 01:為什麼你需要協程,從一個會卡住主執行緒的下載函式說起》那個會卡住主執行緒的下載函式來對照一次,一般函式的簽名大概長這樣:

fun downloadImage(url: String): ByteArray {
    // 函式內部實作,今天先略過
}

改成 suspend function 之後,簽名層級的差異只有一個修飾字:

suspend fun downloadImage(url: String): ByteArray {
    // 函式內部真正如何做到非阻塞,留到後面幾篇再拆解
}

輸入參數一樣,回傳型別一樣,唯一的差別就是那個 suspend。這裡刻意只停在簽名層級的對照,函式內部要如何真正實作非阻塞的網路請求,這篇先不展開,如何呼叫或啟動這個函式,同樣先不展開。

有一件事值得先提醒你,避免你看完這個語法就急著想找地方呼叫它:這個函式即便已經宣告成 suspend function,要真正被呼叫執行,還需要一個協程環境才能啟動。這件事今天先不深入,只在這裡留一個印象,細節等系列往後推進到啟動方式的部分再處理。

暫停的時候,狀態被存到哪裡去了

今天確立了 suspend function 這個系列既定錨點:一個標記了 suspend 修飾字的函式,具備在執行期間暫停並讓出執行緒、之後恢復執行而不阻塞底層執行緒的能力。也破除了一個常見誤解,suspend 本身不是自動變快或自動變非同步的魔法,它是一份資格與合約。更重要的是釐清了暫停跟阻塞的差異,阻塞讓執行緒被迫閒置死等,暫停則讓執行緒可以先去忙別的事,時機到了再回來接續。

但這裡藏著一個還沒解決的問題。suspend function 暫停的時候,一定需要某個東西,把「暫停當下執行到哪一行、接下來要做什麼、區域變數的值是什麼」這些資訊完整記住,不然恢復執行時根本不知道該從哪裡接起。這件事目前還是一個黑盒子。

下一篇會正式打開這個黑盒子,定案一個新的關鍵詞,解釋編譯器在背後偷偷做了什麼轉換,才讓 suspend function 具備了今天看到的這種暫停能力,詳見 《Day 03:Continuation 是什麼,編譯器在背後偷偷做了什麼事》。


上一篇
Day 01:為什麼你需要協程,從一個會卡住主執行緒的下載函式說起
下一篇
Day 03:Continuation 是什麼,編譯器在背後偷偷做了什麼事
系列文
異步程式設計修煉:Kotlin Coroutines 完全指南4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言