生產者-消費者模式

簡介
  言歸正傳!在實際的軟件開發過程中,經常會碰到如下場景:某個模塊負責產生數據,這些數據由另一個模塊來負責處理(此處的模塊是廣義的,可以是類、函數、線程、進程等)。產生數據的模塊,就形象地稱爲生產者;而處理數據的模塊,就稱爲消費者
  單單抽象出生產者和消費者,還夠不上是生產者/消費者模式。該模式還需要有一個緩衝區處於生產者和消費者之間,作爲一箇中介。生產者把數據放入緩衝區,而消費者從緩衝區取出數據。大概的結構如下圖。


  爲了不至於太抽象,我們舉一個寄信的例子(雖說這年頭寄信已經不時興,但這個例子還是比較貼切的)。假設你要寄一封平信,大致過程如下:
  1、你把信寫好——相當於生產者製造數據
  2、你把信放入郵筒——相當於生產者把數據放入緩衝區
  3、郵遞員把信從郵筒取出——相當於消費者把數據取出緩衝區
  4、郵遞員把信拿去郵局做相應的處理——相當於消費者處理數據

 

優點
  可能有同學會問了:這個緩衝區有什麼用捏?爲什麼不讓生產者直接調用消費者的某個函數,直接把數據傳遞過去?搞出這麼一個緩衝區作甚?
  其實這裏面是大有講究的,大概有如下一些好處。
1.解耦
  假設生產者和消費者分別是兩個類。如果讓生產者直接調用消費者的某個方法,那麼生產者對於消費者就會產生依賴(也就是耦合)。將來如果消費者的代碼發生變化,可能會影響到生產者。而如果兩者都依賴於某個緩衝區,兩者之間不直接依賴,耦合也就相應降低了。
  接着上述的例子,如果不使用郵筒(也就是緩衝區),你必須得把信直接交給郵遞員。有同學會說,直接給郵遞員不是挺簡單的嘛?其實不簡單,你必須得認識誰是郵遞員,才能把信給他(光憑身上穿的制服,萬一有人假冒,就慘了)。這就產生和你和郵遞員之間的依賴(相當於生產者和消費者的耦合)。萬一哪天郵遞員換人了,你還要重新認識一下(相當於消費者變化導致修改生產者代碼)。而郵筒相對來說比較固定,你依賴它的成本就比較低(相當於和緩衝區之間的耦合)。
2. 支持併發(concurrency)
  生產者直接調用消費者的某個方法,還有另一個弊端。由於函數調用是同步的(或者叫阻塞的),在消費者的方法沒有返回之前,生產者只好一直等在那邊。萬一消費者處理數據很慢,生產者就會白白糟蹋大好時光。
  使用了生產者/消費者模式之後,生產者和消費者可以是兩個獨立的併發主體(常見併發類型有進程和線程兩種,後面的帖子會講兩種併發類型下的應用)。生產者把製造出來的數據往緩衝區一丟,就可以再去生產下一個數據。基本上不用依賴消費者的處理速度。
  其實當初這個模式,主要就是用來處理併發問題的。
  從寄信的例子來看。如果沒有郵筒,你得拿着信傻站在路口等郵遞員過來收(相當於生產者阻塞);又或者郵遞員得挨家挨戶問,誰要寄信(相當於消費者輪詢)。不管是哪種方法,都挺土的。
3.支持忙閒不均
  緩衝區還有另一個好處。如果製造數據的速度時快時慢,緩衝區的好處就體現出來了。當數據製造快的時候,消費者來不及處理,未處理的數據可以暫時存在緩衝區中。等生產者的製造速度慢下來,消費者再慢慢處理掉。
  爲了充分複用,我們再拿寄信的例子來說事。假設郵遞員一次只能帶走1000封信。萬一某次碰上情人節(也可能是聖誕節)送賀卡,需要寄出去的信超過1000封,這時候郵筒這個緩衝區就派上用場了。郵遞員把來不及帶走的信暫存在郵筒中,等下次過來時再拿走。

啥是數據單元
  何謂數據單元捏?簡單地說,每次生產者放到緩衝區的,就是一個數據單元;每次消費者從緩衝區取出的,也是一個數據單元。對於寄信的例子,我們可以把每一封單獨的信件看成是一個數據單元。

數據單元的特性
1.關聯到業務對象
  首先,數據單元必須關聯到某種業務對象。在考慮該問題的時候,你必須深刻理解當前這個生產者/消費者模式所對應的業務邏輯,才能夠作出合適的判斷。
  由於“寄信”這個業務邏輯比較簡單,所以大夥兒很容易就可以判斷出數據單元是啥。但現實生活中,往往沒這麼樂觀。大多數業務邏輯都比較複雜,當中包含的業務對象是層次繁多、類型各異。在這種情況下,就不易作出決策了。
  這一步很重要,如果選錯了業務對象,會導致後續程序設計和編碼實現的複雜度大爲上升,增加了開發和維護成本。
2.完整性
  所謂完整性,就是在傳輸過程中,要保證該數據單元的完整。要麼整個數據單元被傳遞到消費者,要麼完全沒有傳遞到消費者。不允許出現部分傳遞的情形。
  對於寄信來說,你不能把半封信放入郵筒;同樣的,郵遞員從郵筒中拿信,也不能只拿出信的一部分。
3.獨立性
  所謂獨立性,就是各個數據單元之間沒有互相依賴,某個數據單元傳輸失敗不應該影響已經完成傳輸的單元;也不應該影響尚未傳輸的單元。
  爲啥會出現傳輸失敗捏?假如生產者的生產速度在一段時間內一直超過消費者的處理速度,那就會導致緩衝區不斷增長並達到上限,之後的數據單元就會被丟棄。如果數據單元相互獨立,等到生產者的速度降下來之後,後續的數據單元繼續處理,不會受到牽連;反之,如果數據單元之間有某種耦合,導致被丟棄的數據單元會影響到後續其它單元的處理,那就會使程序邏輯變得非常複雜。
  對於寄信來說,某封信弄丟了,不會影響後續信件的送達;當然更不會影響已經送達的信件。
4.顆粒度
  前面提到,數據單元需要關聯到某種業務對象。那麼數據單元和業務對象是否要一一對應?很多場合確實是一一對應的。
  不過,有時出於性能等因素的考慮,也可能會把N個業務對象打包成一個數據單元。那麼,這個N該如何取值就是顆粒度的考慮了。顆粒度的大小是有講究的。太大的顆粒度可能會造成某種浪費;太小的顆粒度可能會造成性能問題。顆粒度的權衡要基於多方面的因素,以及一些經驗值的考量。
  還是拿寄信的例子。如果顆粒度過小(比如設定爲1),那郵遞員每次只取出1封信。如果信件多了,那就得來回跑好多趟,浪費了時間。
  如果顆粒度太大(比如設定爲100),那寄信的人得等到湊滿100封信纔拿去放入郵筒。假如平時很少寫信,就得等上很久,也不太爽。
  可能有同學會問:生產者和消費者的顆粒度能否設置成不同大小(比如對於寄信人設置成1,對於郵遞員設置成100)。當然,理論上可以這麼幹,但是在某些情況下會增加程序邏輯和代碼實現的複雜度。後面討論具體技術細節時,或許會聊到這個問題。

發佈了26 篇原創文章 · 獲贊 1 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章