iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0

Day 21:StateFlow 與 SharedFlow,冷流不夠用的時候 停在一個新的情境上:如果一個協程產生了一批資料,需要一筆一筆像接力一樣,直接遞交給另一個協程去處理,而不是誰想看就自己訂閱,這種需求跟 StateFlow 持有狀態、SharedFlow 廣播事件都不太一樣。

今天要正面回答這個問題。

訂閱之外,還有一種接力式的傳遞方式

Day 21 建立的 StateFlow、SharedFlow 心智模型,核心都是「訂閱」,誰想看,就自己訂閱去看。但如果情境換成一個協程做完一步,就要把結果直接交到下一個協程手上,而不是廣播給誰想看的人,這件事聽起來完全是另一套邏輯。

Kotlin 協程提供一種專門用於協程之間傳遞資料的通訊原語,具備發送端與接收端,這就是 Channel。這個詞從今天起正式定案,全系列後續一律使用 Channel 這個英文原詞,不另外翻譯。

發送端與接收端,一次只交給一個人

一個 Channel 具備發送端與接收端兩個角色。發送端把資料送進 Channel,接收端從 Channel 取出資料。這裡有一個關鍵規則,同一筆資料一旦被某個接收端取走,就不會再被其他接收端取走。這跟 SharedFlow 讓所有訂閱者都收到同一份事件的廣播模式,是明顯不同的兩種運作方式。

拿多來源圖片下載與聚合工具具體化這件事。假設設計成一個協程專門負責從多個來源依序把下載完成的圖片資料送進一個 Channel,另一個協程專門負責從這個 Channel 依序取出圖片資料,做後續的壓縮或儲存處理。這兩個協程之間不需要直接互相呼叫,也不需要互相等待對方,只需要透過這個 Channel 完成資料的接力遞交,負責下載的協程專心把圖片送進去,負責處理的協程專心把圖片取出來,各自做各自的事。

這裡可以借用一個比喻,Channel 像工廠裡的一條傳送帶,一端把貨物放上去,另一端把貨物取下來,貨物一旦被取走,就不會再出現在傳送帶上給第二個人拿。這個比喻只用來描述點對點遞交這個特性,跟 Day 21 廣播電台那個比喻描述的是完全不同的兩種畫面,廣播電台是同一個節目,所有轉到這個頻道的人都聽得到,傳送帶則是一件貨物只會被一個人拿走。

回到跟 Day 21 的對照,這組差異值得說清楚。StateFlow、SharedFlow 是訂閱式的,訂閱者主動訂閱之後,被動接收分享的狀態或事件,同一份狀態或事件可能同時分享給多個訂閱者。Channel 是點對點遞交式的,一筆資料只會被一個接收端取走。這是協程間傳遞資料的兩種不同模式,適合的情境也不一樣,並不是其中一個可以取代另一個,遇到需要多個地方同時知道同一個狀態或事件的情境,答案還是 StateFlow 或 SharedFlow;遇到資料需要一筆一筆接力交給下一個處理者,且不需要被多方共享的情境,Channel 才是合適的選擇。

送出與接收,都可能是需要等待的動作

Channel 的發送與接收動作,並不是單純的資料結構操作,把值放進去、把值拿出來這麼簡單。這兩個動作本身建立在 Day 02:第一個 suspend function,暫停到底暫停了什麼 定案的 suspend function 之上,都是可能暫停的操作。

如果 Channel 內部容量已滿,發送端會暫停等待,直到有空間或有人接收;如果 Channel 目前沒有資料,接收端會暫停等待,直到有新資料送進來。這個暫停等待的過程,延續了系列一路建立的「暫停不等於阻塞」心智模型,執行緒沒有被死死佔住,其他協程仍能正常運作,只是這個發送或接收的動作,記下了目前執行到哪裡,等條件滿足了再接續下去。

一個極簡的語法樣貌大致是這樣:

val channel = Channel<ByteArray>()

// 發送端協程
launch {
    val imageData = downloadImage(url)
    channel.send(imageData)
}

// 接收端協程
launch {
    val imageData = channel.receive()
    processImage(imageData)
}

send 把一筆資料送進 Channel,receive 把一筆資料取出來,兩者都是 suspend function,都可能在條件不滿足時暫停等待。這裡也可以簡單提一下,接收端還能持續依序從 Channel 取出資料,直到某個結束訊號出現,讓接收端用一種類似依序處理一串值的方式運作。這個方向性認識先記在這裡就好,具體語法細節今天不展開,留到 Day 23 談 Flow 收集方式的時候再一併釐清,避免現在就把兩者混在一起看。

Channel 依然活在結構化並發的框架裡

看到這裡,可能會覺得 Channel 是一種獨立於協程生命週期之外的技術,這個印象需要澄清。

負責發送與接收的協程,依然是在某個 Coroutine Scope 裡啟動的協程,依然遵守 Day 07:Structured Concurrency,為什麼協程不能亂長亂放 定案的父子收斂規則。Channel 本身只是這些協程之間傳遞資料的管道,並不會讓協程脫離既有的生命週期管理。負責發送或接收的協程如果被取消,這個發送或接收的動作,也會依循既有的取消機制結束,不會繼續卡在那裡死等。

這裡也留一個方向性提醒。使用 Channel 時,需要留意由誰負責在資料傳遞完畢後關閉這個管道,讓接收端知道不會再有新資料進來。這件事今天只建立「需要有人負責結束這個管道」的印象,完整的資源管理細節不在今天展開。

Flow 的收集,會不會也是某種接力

今天知道了 Channel 讓一個協程可以把資料一筆一筆,像接力一樣遞交給另一個協程,發送與接收的動作本身也遵循暫停不等於阻塞的既有心智模型,而且這一切依然活在結構化並發的框架裡。

回頭想想 Day 19:從回傳一個值到回傳一串值,Flow 是什麼 收集 Flow 的時候,資料也是依序一筆一筆被交到收集端手中。這兩種情境聽起來似乎有些相似,一個是 Channel 的發送端把資料交給接收端,一個是 Flow 把資料交給收集它的那一方。Flow 底層會不會其實也用到了類似 Channel 這樣的機制?

下一篇會正式回答這個問題,詳見 《Day 23:Flow 底層其實是 Channel,兩者的關係是什麼》。


上一篇
Day 21:StateFlow 與 SharedFlow,冷流不夠用的時候
下一篇
Day 23:Flow 底層其實是 Channel,兩者的關係是什麼
系列文
異步程式設計修煉:Kotlin Coroutines 完全指南 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言