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

2022-10-15 04:54:06 字數 4869 閱讀 2215

[0]:概述

今天打算來介紹一下「生產者/消費者模式」,這玩意兒在很多開發領域都能派上用場。由於該模式很重要,打算分幾個帖子來介紹。今天這個帖子先來掃盲一把。

如果你對這個模式已經比較了解,請跳過本掃盲帖,直接看下乙個帖子(關於該模式的具體應用)。

看到這裡,可能有同學心中犯嘀咕了:在四人幫(gof)的23種模式裡面似乎沒聽說過這種嘛!其實gof那經典的23種模式主要是基於oo的(從書名《design patterns:

elements of reusable object-oriented software》就可以看出來)。而pattern實際上即可以是oo的pattern,也可以是非oo的pattern的。

★簡介言歸正傳!在實際的軟體開發過程中,經常會碰到如下場景:某個模組負責產生資料,這些資料由另乙個模組來負責處理(此處的模組是廣義的,可以是類、函式、執行緒、程序等)。

產生資料的模組,就形象地稱為生產者;而處理資料的模組,就稱為消費者。

單單抽象出生產者和消費者,還夠不上是生產者/消費者模式。該模式還需要有乙個緩衝區處於生產者和消費者之間,作為乙個中介。生產者把資料放入緩衝區,而消費者從緩衝區取出資料。

大概的結構如下圖。

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

1、你把信寫好——相當於生產者製造資料

2、你把信放入郵筒——相當於生產者把資料放入緩衝區

3、郵遞員把信從郵筒取出——相當於消費者把資料取出緩衝區

4、郵遞員把信拿去郵局做相應的處理——相當於消費者處理資料

★優點可能有同學會問了:這個緩衝區有什麼用捏?為什麼不讓生產者直接呼叫消費者的某個函式,直接把資料傳遞過去?搞出這麼乙個緩衝區作甚?

其實這裡面是大有講究的,大概有如下一些好處。

◇解耦假設生產者和消費者分別是兩個類。如果讓生產者直接呼叫消費者的某個方法,那麼生產者對於消費者就會產生依賴(也就是耦合)。將來如果消費者的**發生變化,可能會影響到生產者。

而如果兩者都依賴於某個緩衝區,兩者之間不直接依賴,耦合也就相應降低了。

接著上述的例子,如果不使用郵筒(也就是緩衝區),你必須得把信直接交給郵遞員。有同學會說,直接給郵遞員不是挺簡單的嘛?其實不簡單,你必須得認識誰是郵遞員,才能把信給他(光憑身上穿的**,萬一有人假冒,就慘了)。

這就產生和你和郵遞員之間的依賴(相當於生產者和消費者的強耦合)。萬一哪天郵遞員換人了,你還要重新認識一下(相當於消費者變化導致修改生產者**)。而郵筒相對來說比較固定,你依賴它的成本就比較低(相當於和緩衝區之間的弱耦合)。

◇支援併發(concurrency)

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

使用了生產者/消費者模式之後,生產者和消費者可以是兩個獨立的併發主體(常見併發型別有程序和執行緒兩種,後面的帖子會講兩種併發型別下的應用)。生產者把製造出來的資料往緩衝區一丟,就可以再去生產下乙個資料。基本上不用依賴消費者的處理速度。

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

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

◇支援忙閒不均

緩衝區還有另乙個好處。如果製造資料的速度時快時慢,緩衝區的好處就體現出來了。當資料製造快的時候,消費者來不及處理,未處理的資料可以暫時存在緩衝區中。

等生產者的製造速度慢下來,消費者再慢慢處理掉。

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

郵遞員把來不及帶走的信暫存在郵筒中,等下次過來時再拿走。

費了這麼多口水,希望原先不太了解生產者/消費者模式的同學能夠明白它是怎麼一回事。然後在下乙個帖子中,我們來說說如何確定資料單元。

另外,為了方便閱讀,把本系列帖子的目錄整理如下:

1、如何確定資料單元

2、佇列緩衝區

3、佇列緩衝區

4、雙緩衝區

5、......

[1]:如何確定資料單元?

既然前乙個帖子已經搞過掃盲了,那接下來應該開始聊一些具體的程式設計技術問題了。不過在進入具體的技術細節之前,咱們先要搞明白乙個問題:如何確定資料單元?

只有把資料單元分析清楚,後面的技術設計才好搞。

★啥是資料單元

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

不過光這麼介紹,太過於簡單,無助於大夥兒分析出這玩意兒。所以,後面咱們來看一下資料單元需要具備哪些特性。搞明白這些特性之後,就容易從複雜的業務邏輯中分析出適合做資料單元的東西了。

★資料單元的特性

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

◇關聯到業務物件

首先,資料單元必須關聯到某種業務物件。在考慮該問題的時候,你必須深刻理解當前這個生產者/消費者模式所對應的業務邏輯,才能夠作出合適的判斷。

由於「寄信」這個業務邏輯比較簡單,所以大夥兒很容易就可以判斷出資料單元是啥。但現實生活中,往往沒這麼樂觀。大多數業務邏輯都比較複雜,當中包含的業務物件是層次繁多、型別各異。

在這種情況下,就不易作出決策了。

這一步很重要,如果選錯了業務物件,會導致後續程式設計和編碼實現的複雜度大為上公升,增加了開發和維護成本。

◇完整性

所謂完整性,就是在傳輸過程中,要保證該資料單元的完整。要麼整個資料單元被傳遞到消費者,要麼完全沒有傳遞到消費者。不允許出現部分傳遞的情形。

對於寄信來說,你不能把半封信放入郵筒;同樣的,郵遞員從郵筒中拿信,也不能只拿出信的一部分。

◇獨立性

所謂獨立性,就是各個資料單元之間沒有互相依賴,某個資料單元傳輸失敗不應該影響已經完成傳輸的單元;也不應該影響尚未傳輸的單元。

為啥會出現傳輸失敗捏?假如生產者的生產速度在一段時間內一直超過消費者的處理速度,那就會導致緩衝區不斷增長並達到上限,之後的資料單元就會被丟棄。如果資料單元相互獨立,等到生產者的速度降下來之後,後續的資料單元繼續處理,不會受到牽連;反之,如果資料單元之間有某種耦合,導致被丟棄的資料單元會影響到後續其它單元的處理,那就會使程式邏輯變得非常複雜。

對於寄信來說,某封信弄丟了,不會影響後續信件的送達;當然更不會影響已經送達的信件。

◇顆粒度

前面提到,資料單元需要關聯到某種業務物件。那麼資料單元和業務物件是否要一一對應捏?很多場合確實是一一對應的。

不過,有時出於效能等因素的考慮,也可能會把n個業務物件打包成乙個資料單元。那麼,這個n該如何取值就是顆粒度的考慮了。顆粒度的大小是有講究的。

太大的顆粒度可能會造成某種浪費;太小的顆粒度可能會造成效能問題。顆粒度的權衡要基於多方面的因素,以及一些經驗值的考量。

還是拿寄信的例子。如果顆粒度過小(比如設定為1),那郵遞員每次只取出1封信。如果信件多了,那就得來回跑好多趟,浪費了時間。

如果顆粒度太大(比如設定為100),那寄信的人得等到湊滿100封信才拿去放入郵筒。假如平時很少寫信,就得等上很久,也不太爽。

可能有同學會問:生產者和消費者的顆粒度能否設定成不同大小(比如對於寄信人設定成1,對於郵遞員設定成100)。當然,理論上可以這麼幹,但是在某些情況下會增加程式邏輯和**實現的複雜度。

後面討論具體技術細節時,或許會聊到這個問題。

好,資料單元的話題就說到這。希望通過本帖子,大夥兒能夠搞明白資料單元到底是怎麼一回事。下乙個帖子,咱們來聊一下「基於佇列的緩衝區」,技術上如何實現。

[2]:佇列緩衝區

經過前面兩個帖子的鋪墊,今天終於開始聊一些具體的程式設計技術了。由於不同的緩衝區型別、不同的併發場景對於具體的技術實現有較大的影響。為了深入淺出、便於大夥兒理解,咱們先來介紹最傳統、最常見的方式。

也就是單個生產者對應單個消費者,當中用佇列(fifo)作緩衝。

關於併發的場景,在之前的帖子「程序還執行緒?是乙個問題!」中,已經專門論述了程序和執行緒各自的優缺點,兩者皆不可偏廢。

所以,後面對各種緩衝區型別的介紹都會同時提及程序方式和執行緒方式。

★執行緒方式

先來說一下併發執行緒中使用佇列的例子,以及相關的優缺點。

◇記憶體分配的效能

**程方式下,生產者和消費者各自是乙個執行緒。生產者把資料寫入佇列頭(以下簡稱push),消費者從佇列尾部讀出資料(以下簡稱pop)。當隊列為空,消費者就稍息(稍事休息);當佇列滿(達到最大長度),生產者就稍息。

整個流程並不複雜。

那麼,上述過程會有什麼問題捏?乙個主要的問題是關於記憶體分配的效能開銷。對於常見的佇列實現:

在每次push時,可能涉及到堆記憶體的分配;在每次pop時,可能涉及堆記憶體的釋放。假如生產者和消費者都很勤快,頻繁地push、pop,那記憶體分配的開銷就很可觀了。對於記憶體分配的開銷,用j**a的同學可以參見前幾天的帖子「j**a效能優化[1]」;對於用c/c++的同學,想必對os底層機制會更清楚,應該知道分配堆記憶體(new或malloc)會有加鎖的開銷和使用者態/核心態切換的開銷。

那該怎麼辦捏?請聽下文分解,關於「生產者/消費者模式[3]:環形緩衝區」。

◇同步和互斥的效能

另外,由於兩個執行緒共用乙個佇列,自然就會涉及到執行緒間諸如同步啊、互斥啊、死鎖啊等等勞心費神的事情。好在"作業系統"這門課程對此有詳細介紹,學過的同學應該還有點印象吧?對於沒學過這門課的同學,也不必難過,網上相關的介紹挺多的(比如"這裡"),大夥自己去瞅一瞅。

關於這方面的細節,咱今天就不多囉嗦了。

這會兒要細談的是,同步和互斥的效能開銷。在很多場合中,諸如訊號量、互斥量等玩意兒的使用也是有不小的開銷的(某些情況下,也可能導致使用者態/核心態切換)。如果像剛才所說,生產者和消費者都很勤快,那這些開銷也不容小覷啊。

這又該咋辦捏?請聽下文的下文分解,關於「生產者/消費者模式[4]:雙緩衝區」。

如何把握消費者需求設計產品

要想設計出極致的產品,首先我們應該熟悉深入了解使用者的思維,我們更應該將使用者的需求演變成具體的消費場景,而且切合實際符合消費者的心理需求,才能勾起消費者使用的慾望。想想我們看廣告時的感覺,那些一味宣傳強大功能的產品,有幾個能讓我們駐足?培訓講師拿一款椅子銷售來說,擺設了三個場景 1.使用者痛點的深...

維護消費者權益的途徑教學設計

一 教材分析 一 課程標準 內容要求 1 學生要知道法律維護消費者的合法權益。2 學會運用法律維護自己作為消費者的權利。二 教材的地位 第七單元 我們的文化經濟權利 強調了文化權利和經濟權利,本單元介紹的消費者權益保 在我國法律體系中有著重要的地位,它是一部綜合的 完備的 專門保護消費者權益的法律,...

《生產消費者力量比爾 奎恩讀後感

生產消費者力量 比爾.奎恩讀後感 我們每個人都是消費者,每天都要去消費,請你思考下面幾個問題 你真正想要擁有的是什麼?折扣還是財務自由?你是願意向一家為了吸引顧客而花費數百萬美金請明星做廣告的公司購物,還是願意向一家不做廣告,而將這些錢以獎金的方式支付給生產消費者的公司購物呢?如果能夠以公道合理的 ...