架構設計:生產者/消費者模式

概述

今天打算來介紹一下“生產者/消費者模式”,這玩意兒在很多開發領域都能派上用場。
看到這里,可能有同學心中犯嘀咕了:在四人幫(GOF)的23種模式里面似乎沒聽說過這種嘛!其實GOF那經典的23種模式主要是基于OO的(從書名《Design Patterns: Elements of Reusable Object-Oriented Software》就可以看出來)。而Pattern實際上即可以是OO的Pattern,也可以是非OO的Pattern的。

常見場景

某個模塊負責產生數據,這些數據由另一個模塊來負責處理。產生數據的模塊,就形象地稱為生產者;而處理數據的模塊,就稱為消費者。
該模式還需要有一個緩沖區處于生產者和消費者之間,作為一個中介。生產者把數據放入緩沖區,而消費者從緩沖區取出數據

緩沖區作用

  1. 解耦,生產者和消費者只依賴緩沖區,而不互相依賴;
  2. 支持并發和異步。

簡介

言歸正傳!在實際的軟件開發過程中,經常會碰到如下場景:某個模塊負責產生數據,這些數據由另一個模塊來負責處理(此處的模塊是廣義的,可以是類、函數、線程、進程等)。產生數據的模塊,就形象地稱為生產者;而處理數據的模塊,就稱為消費者

單單抽象出生產者和消費者,還夠不上是生產者/消費者模式。該模式還需要有一個緩沖區處于生產者和消費者之間,作為一個中介。生產者把數據放入緩沖區,而消費者從緩沖區取出數據。大概的結構如下圖。

為了不至于太抽象,我們舉一個寄信的例子(雖說這年頭寄信已經不時興,但這個例子還是比較貼切的)。假設你要寄一封平信,大致過程如下:

  1. 你把信寫好——相當于生產者制造數據
  2. 你把信放入郵筒——相當于生產者把數據放入緩沖區
  3. 郵遞員把信從郵筒取出——相當于消費者把數據取出緩沖區
  4. 郵遞員把信拿去郵局做相應的處理——相當于消費者處理數據

生產消費者模型

生產者消費者模型具體來講,就是在一個系統中,存在生產者和消費者兩種角色,他們通過內存緩沖區進行通信,生產者生產消費者需要的資料,消費者把資料做成產品。生產消費者模式如下圖。

生產消費者模型

在日益發展的服務類型中,譬如注冊用戶這種服務,它可能解耦成好幾種獨立的服務(賬號驗證,郵箱驗證碼,手機短信碼等)。它們作為消費者,等待用戶輸入數據,在前臺數據提交之后會經過分解并發送到各個服務所在的url,分發的那個角色就相當于生產者。消費者在獲取數據時候有可能一次不能處理完,那么它們各自有一個請求隊列,那就是內存緩沖區了。做這項工作的框架叫做消息隊列。

優點

可能有同學會問了:這個緩沖區有什么用捏?為什么不讓生產者直接調用消費者的某個函數,直接把數據傳遞過去?搞出這么一個緩沖區作甚?
其實這里面是大有講究的,大概有如下一些好處。

  • 解耦

假設生產者和消費者分別是兩個類。如果讓生產者直接調用消費者的某個方法,那么生產者對于消費者就會產生依賴(也就是耦合)。將來如果消費者的代碼發生變化,可能會影響到生產者。而如果兩者都依賴于某個緩沖區,兩者之間不直接依賴,耦合也就相應降低了。

接著上述的例子,如果不使用郵筒(也就是緩沖區),你必須得把信直接交給郵遞員。有同學會說,直接給郵遞員不是挺簡單的嘛?其實不簡單,你必須得認識誰是郵遞員,才能把信給他(光憑身上穿的制服,萬一有人假冒,就慘了)。這就產生和你和郵遞員之間的依賴(相當于生產者和消費者的強耦合)。萬一哪天郵遞員換人了,你還要重新認識一下(相當于消費者變化導致修改生產者代碼)。而郵筒相對來說比較固定,你依賴它的成本就比較低(相當于和緩沖區之間的弱耦合)

  • 支持并發(concurrency)

生產者直接調用消費者的某個方法,還有另一個弊端。由于函數調用是同步的(或者叫阻塞的),在消費者的方法沒有返回之前,生產者只好一直等在那邊。萬一消費者處理數據很慢,生產者就會白白糟蹋大好時光。

使用了生產者/消費者模式之后,生產者和消費者可以是兩個獨立的并發主體(常見并發類型有進程和線程兩種,后面的帖子會講兩種并發類型下的應用)。生產者把制造出來的數據往緩沖區一丟,就可以再去生產下一個數據。基本上不用依賴消費者的處理速度。

其實當初這個模式,主要就是用來處理并發問題的。

從寄信的例子來看。如果沒有郵筒,你得拿著信傻站在路口等郵遞員過來收(相當于生產者阻塞);又或者郵遞員得挨家挨戶問,誰要寄信(相當于消費者輪詢)。不管是哪種方法,都挺土的

  • 支持忙閑不均

緩沖區還有另一個好處。如果制造數據的速度時快時慢,緩沖區的好處就體現出來了。當數據制造快的時候,消費者來不及處理,未處理的數據可以暫存在緩沖區中。等生產者的制造速度慢下來,消費者再慢慢處理掉。

為了充分復用,我們再拿寄信的例子來說事。假設郵遞員一次只能帶走1000封信。萬一某次碰上情人節(也可能是圣誕節)送賀卡,需要寄出去的信超過1000封,這時候郵筒這個緩沖區就派上用場了。郵遞員把來不及帶走的信暫存在郵筒中,等下次過來時再拿走。

如何確定數據單元?

既然前一個帖子已經搞過掃盲了,那接下來應該開始聊一些具體的編程技術問題了。不過在進入具體的技術細節之前,咱們先要搞明白一個問題:如何確定數據單元?只有把數據單元分析清楚,后面的技術設計才好搞。

啥是數據單元

何謂數據單元捏?簡單地說,每次生產者放到緩沖區的,就是一個數據單元;每次消費者從緩沖區取出的,也是一個數據單元。對于前一個帖子中寄信的例子,我們可以把每一封單獨的信件看成是一個數據單元。
不過光這么介紹,太過于簡單,無助于大伙兒分析出這玩意兒。所以,后面咱們來看一下數據單元需要具備哪些特性。搞明白這些特性之后,就容易從復雜的業務邏輯中分析出適合做數據單元的東西了。

數據單元的特性

分析數據單元,需要考慮如下幾個方面的特性:

  • 關聯到業務對象

首先,數據單元必須關聯到某種業務對象。在考慮該問題的時候,你必須深刻理解當前這個生產者/消費者模式所對應的業務邏輯,才能夠作出合適的判斷。
由于“寄信”這個業務邏輯比較簡單,所以大伙兒很容易就可以判斷出數據單元是啥。但現實生活中,往往沒這么樂觀。大多數業務邏輯都比較復雜,當中包含的業務對象是層次繁多、類型各異。在這種情況下,就不易作出決策了。
這一步很重要,如果選錯了業務對象,會導致后續程序設計和編碼實現的復雜度大為上升,增加了開發和維護成本。

  • 完整性

所謂完整性,就是在傳輸過程中,要保證該數據單元的完整。要么整個數據單元被傳遞到消費者,要么完全沒有傳遞到消費者。不允許出現部分傳遞的情形。
對于寄信來說,你不能把半封信放入郵筒;同樣的,郵遞員從郵筒中拿信,也不能只拿出信的一部分。

  • 獨立性

所謂獨立性,就是各個數據單元之間沒有互相依賴,某個數據單元傳輸失敗不應該影響已經完成傳輸的單元;也不應該影響尚未傳輸的單元。
為啥會出現傳輸失敗捏?假如生產者的生產速度在一段時間內一直超過消費者的處理速度,那就會導致緩沖區不斷增長并達到上限,之后的數據單元就會被丟棄。如果數據單元相互獨立,等到生產者的速度降下來之后,后續的數據單元繼續處理,不會受到牽連;反之,如果數據單元之間有某種耦合,導致被丟棄的數據單元會影響到后續其它單元的處理,那就會使程序邏輯變得非常復雜。
對于寄信來說,某封信弄丟了,不會影響后續信件的送達;當然更不會影響已經送達的信件。

  • 顆粒度

前面提到,數據單元需要關聯到某種業務對象。那么數據單元和業務對象是否要一一對應捏?很多場合確實是一一對應的。
不過,有時出于性能等因素的考慮,也可能會把N個業務對象打包成一個數據單元。那么,這個N該如何取值就是顆粒度的考慮了。顆粒度的大小是有講究的。太大的顆粒度可能會造成某種浪費;太小的顆粒度可能會造成性能問題。顆粒度的權衡要基于多方面的因素,以及一些經驗值的考量。
還是拿寄信的例子。如果顆粒度過小(比如設定為1),那郵遞員每次只取出1封信。如果信件多了,那就得來回跑好多趟,浪費了時間。
如果顆粒度太大(比如設定為100),那寄信的人得等到湊滿100封信才拿去放入郵筒。假如平時很少寫信,就得等上很久,也不太爽。
可能有同學會問:生產者和消費者的顆粒度能否設置成不同大小(比如對于寄信人設置成1,對于郵遞員設置成100)。當然,理論上可以這么干,但是在某些情況下會增加程序邏輯和代碼實現的復雜度。后面討論具體技術細節時,或許會聊到這個問題。
好,數據單元的話題就說到這。希望通過本帖子,大伙兒能夠搞明白數據單元到底是怎么一回事。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容