iT邦幫忙

2026 iThome 鐵人賽

DAY 25
1
Software Development

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

Day 24:背壓是什麼,生產太快時該怎麼踩煞車

  • 分享至 

  • xImage
  •  

Day 23:Flow 底層其實是 Channel,兩者的關係是什麼 停在一個問題上:今天談到的暫停等待,處理的都是速度差距還在合理範圍內的情境,如果生產資料的速度天生就比消費資料的速度快上許多,光靠暫停等待這一招,真的能應付所有這類情境嗎?

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

暫停等待,是背壓最原始的樣子

Day 23 建立的結論很明確:某些建立 Flow 的方式,底層透過類似 Channel 的機制完成值的暫停發送與依序接收,生產速度與消費速度不一致時,兩端靠著暫停等待自然銜接,開發者不需要自己動手處理。這個結論今天直接拿來用,不重新解釋一次。

當生產者產生資料的速度超過消費者處理速度時,需要有某種機制回頭調節生產速度,這個概念稱為背壓,Backpressure。從今天開始,系列裡談到這種「回頭調節生產速度」的機制,一律使用這個詞。

站在 Day 23 的結論上往回看,會發現一件事:暫停等待,其實正是背壓最基本、最原始的一種形式。生產端被迫暫停等待這件事本身,就已經在替消費端踩了煞車,生產端沒辦法無視消費端的處理進度、一股腦把資料全部塞出去。背壓並不是今天才憑空冒出來的新機制,Flow 與 Channel 從一開始就內建了這種天性,只是 Day 23 還沒有替它取名字,今天要做的,是把這件事正式定案,並接著討論當這種最原始的形式不夠用時,還有哪些更積極的做法。

只是暫停等待,什麼情況會不夠用

暫停等待聽起來已經解決了速度落差的問題,但把情境推到極端一點,會發現它不是萬能解。

回到多來源圖片下載與聚合工具。假設同時有五、六個下載來源,每個來源都以極高的頻率回報自己的下載進度,而負責更新畫面的一方,處理速度遠遠跟不上這種回報頻率。如果堅持每一次進度回報都得排隊等消費端處理完才能繼續,生產端可能會被迫暫停等待非常久。這種等待不會讓程式崩潰,架構本身依然是穩固的,但生產端反應會變得遲緩,使用者實際感受到的,可能是整個下載流程忽快忽慢,甚至有種卡頓的錯覺。

問題核心在於,單純的暫停等待策略,代表「所有資料都必須被完整處理過一輪」,一筆都不能少,一筆都不能跳過。但在某些情境下,開發者其實不需要每一筆資料都被處理到。以進度回報為例,只要使用者最終看到的是最新的進度百分比,中間被跳過的幾筆數值其實無關緊要,沒有人會在意進度條在半秒前曾經短暫顯示過 47% 還是 48%。這種情境如果還死守著「一筆都不能少」的原則,等於是拿最嚴格的標準,去處理一個其實可以容忍取捨的問題,需要更積極的策略介入。

更積極的因應策略,緩衝、節流與只留最新

暫停等待之外,還有幾種常見的方向可以因應生產速度過快的情境,各自代表不同的取捨。

第一種是緩衝策略。做法是讓生產端與消費端之間保留一定的緩衝空間,生產端可以在這個空間範圍內持續產生資料,不必每一筆都立刻觸發暫停,直到緩衝空間也被填滿,才真正輪到生產端暫停等待。這讓短暫的速度落差有一個緩衝餘地,生產端偶爾衝刺一下不會立刻被卡住。這裡只建立方向性認識,緩衝空間該設多大、行為細節怎麼調整,屬於實作層面的參數選擇,今天不展開。

第二種是只保留最新策略。這種策略的精神是,消費端其實只關心「最新的一筆資料是什麼」,中間被生產出來但還沒被消費到的舊資料,可以直接捨棄,不需要排隊等待被處理。回到多來源圖片下載與聚合工具,持續回報下載進度的情境裡,如果消費端還在處理上一筆進度,等它有空的時候,其實只需要拿到目前最新的進度數值即可,中間被跳過的幾筆進度並不影響使用者體驗。這正是 Day 21:StateFlow 與 SharedFlow,冷流不夠用的時候 定案的 StateFlow 那個核心特性,一份隨時可查的最新狀態,只持有目前這一份值,不在意中間經過了哪些變化,在背壓語境下的另一種體現。StateFlow 天生就是這種取捨的具體印證,這也是為什麼進度顯示這類需求,一開始就傾向選擇 StateFlow 而不是逐筆處理的冷流。

第三種是節流策略,思路又不一樣,限制的是生產端更新的頻率本身。不論實際進度變化得多快,都只在固定的時間間隔內回報一次最新進度,直接從源頭減少需要被處理的資料量。這種做法不去動消費端怎麼處理,而是讓生產端自己收斂發送的節奏,同樣屬於方向性認識,具體的節流間隔怎麼設定,留待之後有實際需要時再細看。

可以借用一個比喻理解這幾種策略的取捨精神:水管裡水流太急,水龍頭來不及消化,這時候可以選擇讓水管本身多留一段緩衝空間,或是乾脆讓水龍頭只留意最新流過來的水量,不追究中間流過哪些水。這幾種策略共同的精神,都是在「每一筆資料都不能遺漏地被處理」與「系統要能保持順暢反應」這兩個目標之間,依照情境需求做出取捨。並非每種情境都需要最嚴格的「一筆都不能少」,也並非每種情境都適合隨意捨棄資料,關鍵在於資料本身的性質。

一段極簡的語法示意,大致呈現這幾種策略在寫法上的方向:

// 緩衝:允許生產端在緩衝空間內持續發送,不必每次都立刻等待
progressFlow.buffer()

// 只保留最新:消費端只在意最新一筆,中間值可以被跳過
progressFlow.conflate()

// 節流:固定時間間隔才處理一次最新值
progressFlow.sample(200)

這裡只是讓讀者對「這幾種策略在語法上長什麼樣子」有個方向性的印象,實際參數怎麼調整、各自還有哪些搭配選項,不是今天要處理的範圍。

背壓策略怎麼選,回到情境本身的需求

幾種策略攤開來看,容易讓人不知道該從何選起,這時候可以回到一組簡單的判斷方向。

如果每一筆資料都攸關正確性,遺漏會造成問題,例如財務相關的逐筆紀錄,那麼應該傾向讓生產端暫停等待,或是搭配緩衝策略,確保資料不遺漏,寧可讓生產端偶爾等一下,也不要漏掉任何一筆該處理的資料。如果資料具有「新的會蓋過舊的」這種性質,例如進度回報、狀態更新這類情境,可以傾向使用只保留最新或節流策略,換取系統反應的流暢度,讓使用者感受到的是即時、順暢,而不是被舊資料卡住的遲鈍感。

回到多來源圖片下載與聚合工具,這個示範情境裡剛好同時存在這兩種性質的資料。圖片本身的下載結果攸關正確性,一張圖片下載成功還是失敗,這件事不應該被任何背壓策略捨棄,每一個結果都需要被完整處理。但下載過程中的進度回報數值,屬於可以容忍捨棄中間值的情境,使用者要看到的是流暢更新的進度條,不是每一個小數點都被記錄下來。這正好呼應了系列一路使用的示範情境,同一個工具裡,可能同時存在需要嚴謹處理與可以彈性取捨的不同資料,開發者需要依資料本身的性質分別判斷,而不是對整個系統套用同一種背壓策略。

本階段的資料流工具箱已經到齊

今天正式定案了背壓,Backpressure,這個詞。從 Day 23:Flow 底層其實是 Channel,兩者的關係是什麼 建立的結論出發,指出暫停等待正是背壓最原始的形式,接著看到生產速度遠超消費速度時,暫停等待可能不夠用,還有緩衝、只保留最新、節流這幾種更積極的策略可以選用,選哪一種,取決於資料本身能不能被容忍捨棄。

從 Day 19:從回傳一個值到回傳一串值,Flow 是什麼 的 Flow,Day 20:Flow 的操作符,map、filter、collect 這些常用招式 的操作符,Day 21:StateFlow 與 SharedFlow,冷流不夠用的時候 的 StateFlow 與 SharedFlow,Day 22:Channel,協程之間怎麼傳話 的 Channel,一路走到今天的背壓,本階段用來處理協程資料流的核心工具已經全部到齊。

這幾樣工具彼此之間的關係,各自適合什麼樣的情境,值得回頭整理一次。下一篇會做這件事。


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

尚未有邦友留言

立即登入留言