生产者消费者模式详解(转载)
★簡介
????????在實際的軟件開發(fā)過程中,經(jīng)常會碰到如下場景:某個模塊負責產(chǎn)生數(shù)據(jù),這些數(shù)據(jù)由另一個模塊來負責處理(此處的模塊是廣義的,可以是類、函數(shù)、線程、進程等)。產(chǎn)生數(shù)據(jù)的模塊,就形象地稱為生產(chǎn)者;而處理數(shù)據(jù)的模塊,就稱為消費者。
??? 單單抽象出生產(chǎn)者和消費者,還夠不上是生產(chǎn)者/消費者模式。該模式還需要有一個緩沖區(qū)處于生產(chǎn)者和消費者之間,作為一個中介。生產(chǎn)者把數(shù)據(jù)放入緩沖區(qū),而消費者從緩沖區(qū)取出數(shù)據(jù)。大概的結構如下圖。
??? 為了不至于太抽象,我們舉一個寄信的例子(雖說這年頭寄信已經(jīng)不時興,但這個例子還是比較貼切的)。假設你要寄一封平信,大致過程如下:
??? 1、你把信寫好——相當于生產(chǎn)者制造數(shù)據(jù)
??? 2、你把信放入郵筒——相當于生產(chǎn)者把數(shù)據(jù)放入緩沖區(qū)
??? 3、郵遞員把信從郵筒取出——相當于消費者把數(shù)據(jù)取出緩沖區(qū)
??? 4、郵遞員把信拿去郵局做相應的處理——相當于消費者處理數(shù)據(jù)
??? ★優(yōu)點
??? 可能有同學會問了:這個緩沖區(qū)有什么用捏?為什么不讓生產(chǎn)者直接調用消費者的某個函數(shù),直接把數(shù)據(jù)傳遞過去?搞出這么一個緩沖區(qū)作甚?
??? 其實這里面是大有講究的,大概有如下一些好處。
??? ◇解耦
??? 假設生產(chǎn)者和消費者分別是兩個類。如果讓生產(chǎn)者直接調用消費者的某個方法,那么生產(chǎn)者對于消費者就會產(chǎn)生依賴(也就是耦合)。將來如果消費者的代碼發(fā)生變化,可能會影響到生產(chǎn)者。而如果兩者都依賴于某個緩沖區(qū),兩者之間不直接依賴,耦合也就相應降低了。
??? 接著上述的例子,如果不使用郵筒(也就是緩沖區(qū)),你必須得把信直接交給郵遞員。有同學會說,直接給郵遞員不是挺簡單的嘛?其實不簡單,你必須得認識誰是郵遞員,才能把信給他(光憑身上穿的制服,萬一有人假冒,就慘了)。這就產(chǎn)生和你和郵遞員之間的依賴(相當于生產(chǎn)者和消費者的強耦合)。萬一哪天郵遞員換人了,你還要重新認識一下(相當于消費者變化導致修改生產(chǎn)者代碼)。而郵筒相對來說比較固定,你依賴它的成本就比較低(相當于和緩沖區(qū)之間的弱耦合)。
??? ◇支持并發(fā)(concurrency)
??? 生產(chǎn)者直接調用消費者的某個方法,還有另一個弊端。由于函數(shù)調用是同步的(或者叫阻塞的),在消費者的方法沒有返回之前,生產(chǎn)者只好一直等在那邊。萬一消費者處理數(shù)據(jù)很慢,生產(chǎn)者就會白白糟蹋大好時光。
??? 使用了生產(chǎn)者/消費者模式之后,生產(chǎn)者和消費者可以是兩個獨立的并發(fā)主體(常見并發(fā)類型有進程和線程兩種,后面的帖子會講兩種并發(fā)類型下的應用)。生產(chǎn)者把制造出來的數(shù)據(jù)往緩沖區(qū)一丟,就可以再去生產(chǎn)下一個數(shù)據(jù)。基本上不用依賴消費者的處理速度。
??? 其實當初這個模式,主要就是用來處理并發(fā)問題的。
??? 從寄信的例子來看。如果沒有郵筒,你得拿著信傻站在路口等郵遞員過來收(相當于生產(chǎn)者阻塞);又或者郵遞員得挨家挨戶問,誰要寄信(相當于消費者輪詢)。不管是哪種方法,都挺土的。
??? ◇支持忙閑不均
??? 緩沖區(qū)還有另一個好處。如果制造數(shù)據(jù)的速度時快時慢,緩沖區(qū)的好處就體現(xiàn)出來了。當數(shù)據(jù)制造快的時候,消費者來不及處理,未處理的數(shù)據(jù)可以暫時存在緩沖區(qū)中。等生產(chǎn)者的制造速度慢下來,消費者再慢慢處理掉。
??? 為了充分復用,我們再拿寄信的例子來說事。假設郵遞員一次只能帶走1000封信。萬一某次碰上情人節(jié)(也可能是圣誕節(jié))送賀卡,需要寄出去的信超過1000封,這時候郵筒這個緩沖區(qū)就派上用場了。郵遞員把來不及帶走的信暫存在郵筒中,等下次過來時再拿走。
??? 費了這么多口水,希望原先不太了解生產(chǎn)者/消費者模式的同學能夠明白它是怎么一回事。然后在下一個帖子中,我們來說說如何確定數(shù)據(jù)單元。
??? 另外,為了方便閱讀,把本系列帖子的目錄整理如下:
??? 1、如何確定數(shù)據(jù)單元
??? 2、隊列緩沖區(qū)
??? 3、隊列緩沖區(qū)
??? 4、雙緩沖區(qū)
??? 5、......
??? [1]:如何確定數(shù)據(jù)單元?
??? 既然前一個帖子已經(jīng)搞過掃盲了,那接下來應該開始聊一些具體的編程技術問題了。不過在進入具體的技術細節(jié)之前,咱們先要搞明白一個問題:如何確定數(shù)據(jù)單元?只有把數(shù)據(jù)單元分析清楚,后面的技術設計才好搞。
??? ★啥是數(shù)據(jù)單元
??? 何謂數(shù)據(jù)單元捏?簡單地說,每次生產(chǎn)者放到緩沖區(qū)的,就是一個數(shù)據(jù)單元;每次消費者從緩沖區(qū)取出的,也是一個數(shù)據(jù)單元。對于前一個帖子中寄信的例子,我們可以把每一封單獨的信件看成是一個數(shù)據(jù)單元。
??? 不過光這么介紹,太過于簡單,無助于大伙兒分析出這玩意兒。所以,后面咱們來看一下數(shù)據(jù)單元需要具備哪些特性。搞明白這些特性之后,就容易從復雜的業(yè)務邏輯中分析出適合做數(shù)據(jù)單元的東西了。
??? ★數(shù)據(jù)單元的特性
??? 分析數(shù)據(jù)單元,需要考慮如下幾個方面的特性:
??? ◇關聯(lián)到業(yè)務對象
??? 首先,數(shù)據(jù)單元必須關聯(lián)到某種業(yè)務對象。在考慮該問題的時候,你必須深刻理解當前這個生產(chǎn)者/消費者模式所對應的業(yè)務邏輯,才能夠作出合適的判斷。
??? 由于“寄信”這個業(yè)務邏輯比較簡單,所以大伙兒很容易就可以判斷出數(shù)據(jù)單元是啥。但現(xiàn)實生活中,往往沒這么樂觀。大多數(shù)業(yè)務邏輯都比較復雜,當中包含的業(yè)務對象是層次繁多、類型各異。在這種情況下,就不易作出決策了。
??? 這一步很重要,如果選錯了業(yè)務對象,會導致后續(xù)程序設計和編碼實現(xiàn)的復雜度大為上升,增加了開發(fā)和維護成本。
??? ◇完整性
??? 所謂完整性,就是在傳輸過程中,要保證該數(shù)據(jù)單元的完整。要么整個數(shù)據(jù)單元被傳遞到消費者,要么完全沒有傳遞到消費者。不允許出現(xiàn)部分傳遞的情形。
??? 對于寄信來說,你不能把半封信放入郵筒;同樣的,郵遞員從郵筒中拿信,也不能只拿出信的一部分。
??? ◇獨立性
??? 所謂獨立性,就是各個數(shù)據(jù)單元之間沒有互相依賴,某個數(shù)據(jù)單元傳輸失敗不應該影響已經(jīng)完成傳輸?shù)膯卧?#xff1b;也不應該影響尚未傳輸?shù)膯卧?/span>
??? 為啥會出現(xiàn)傳輸失敗捏?假如生產(chǎn)者的生產(chǎn)速度在一段時間內一直超過消費者的處理速度,那就會導致緩沖區(qū)不斷增長并達到上限,之后的數(shù)據(jù)單元就會被丟棄。如果數(shù)據(jù)單元相互獨立,等到生產(chǎn)者的速度降下來之后,后續(xù)的數(shù)據(jù)單元繼續(xù)處理,不會受到牽連;反之,如果數(shù)據(jù)單元之間有某種耦合,導致被丟棄的數(shù)據(jù)單元會影響到后續(xù)其它單元的處理,那就會使程序邏輯變得非常復雜。
??? 對于寄信來說,某封信弄丟了,不會影響后續(xù)信件的送達;當然更不會影響已經(jīng)送達的信件。
??? ◇顆粒度
??? 前面提到,數(shù)據(jù)單元需要關聯(lián)到某種業(yè)務對象。那么數(shù)據(jù)單元和業(yè)務對象是否要一一對應捏?很多場合確實是一一對應的。
??? 不過,有時出于性能等因素的考慮,也可能會把N個業(yè)務對象打包成一個數(shù)據(jù)單元。那么,這個N該如何取值就是顆粒度的考慮了。顆粒度的大小是有講究的。太大的顆粒度可能會造成某種浪費;太小的顆粒度可能會造成性能問題。顆粒度的權衡要基于多方面的因素,以及一些經(jīng)驗值的考量。
??? 還是拿寄信的例子。如果顆粒度過小(比如設定為1),那郵遞員每次只取出1封信。如果信件多了,那就得來回跑好多趟,浪費了時間。
??? 如果顆粒度太大(比如設定為100),那寄信的人得等到湊滿100封信才拿去放入郵筒。假如平時很少寫信,就得等上很久,也不太爽。
??? 可能有同學會問:生產(chǎn)者和消費者的顆粒度能否設置成不同大小(比如對于寄信人設置成1,對于郵遞員設置成100)。當然,理論上可以這么干,但是在某些情況下會增加程序邏輯和代碼實現(xiàn)的復雜度。后面討論具體技術細節(jié)時,或許會聊到這個問題。
??? 好,數(shù)據(jù)單元的話題就說到這。希望通過本帖子,大伙兒能夠搞明白數(shù)據(jù)單元到底是怎么一回事。下一個帖子,咱們來聊一下“基于隊列的緩沖區(qū)”,技術上如何實現(xiàn)。
??? [2]:隊列緩沖區(qū)
??? 經(jīng)過前面兩個帖子的鋪墊,今天終于開始聊一些具體的編程技術了。由于不同的緩沖區(qū)類型、不同的并發(fā)場景對于具體的技術實現(xiàn)有較大的影響。為了深入淺出、便于大伙兒理解,咱們先來介紹最傳統(tǒng)、最常見的方式。也就是單個生產(chǎn)者對應單個消費者,當中用隊列(FIFO)作緩沖。
??? 關于并發(fā)的場景,在之前的帖子“進程還線程?是一個問題!”中,已經(jīng)專門論述了進程和線程各自的優(yōu)缺點,兩者皆不可偏廢。所以,后面對各種緩沖區(qū)類型的介紹都會同時提及進程方式和線程方式。
??? ★線程方式
??? 先來說一下并發(fā)線程中使用隊列的例子,以及相關的優(yōu)缺點。
??? ◇內存分配的性能
??? 在線程方式下,生產(chǎn)者和消費者各自是一個線程。生產(chǎn)者把數(shù)據(jù)寫入隊列頭(以下簡稱push),消費者從隊列尾部讀出數(shù)據(jù)(以下簡稱pop)。當隊列為空,消費者就稍息(稍事休息);當隊列滿(達到最大長度),生產(chǎn)者就稍息。整個流程并不復雜。
??? 那么,上述過程會有什么問題捏?一個主要的問題是關于內存分配的性能開銷。對于常見的隊列實現(xiàn):在每次push時,可能涉及到堆內存的分配;在每次pop時,可能涉及堆內存的釋放。假如生產(chǎn)者和消費者都很勤快,頻繁地push、pop,那內存分配的開銷就很可觀了。對于內存分配的開銷,用Java的同學可以參見前幾天的帖子“Java性能優(yōu)化[1]”;對于用C/C++的同學,想必對OS底層機制會更清楚,應該知道分配堆內存(new或malloc)會有加鎖的開銷和用戶態(tài)/核心態(tài)切換的開銷。
??? 那該怎么辦捏?請聽下文分解,關于“生產(chǎn)者/消費者模式[3]:環(huán)形緩沖區(qū)”。
??? ◇同步和互斥的性能
??? 另外,由于兩個線程共用一個隊列,自然就會涉及到線程間諸如同步啊、互斥啊、死鎖啊等等勞心費神的事情。好在"操作系統(tǒng)"這門課程對此有詳細介紹,學過的同學應該還有點印象吧?對于沒學過這門課的同學,也不必難過,網(wǎng)上相關的介紹挺多的(比如"這里"),大伙自己去瞅一瞅。關于這方面的細節(jié),咱今天就不多啰嗦了。
??? 這會兒要細談的是,同步和互斥的性能開銷。在很多場合中,諸如信號量、互斥量等玩意兒的使用也是有不小的開銷的(某些情況下,也可能導致用戶態(tài)/核心態(tài)切換)。如果像剛才所說,生產(chǎn)者和消費者都很勤快,那這些開銷也不容小覷啊。
??? 這又該咋辦捏?請聽下文的下文分解,關于“生產(chǎn)者/消費者模式[4]:雙緩沖區(qū)”。
??? ◇適用于隊列的場合
??? 剛才盡批判了隊列的缺點,難道隊列方式就一無是處?非也。由于隊列是很常見的數(shù)據(jù)結構,大部分編程語言都內置了隊列的支持(具體介紹見"這里"),有些語言甚至提供了線程安全的隊列(比如JDK 1.5引入的ArrayBlockingQueue)。因此,開發(fā)人員可以撿現(xiàn)成,避免了重新發(fā)明輪子。
??? 所以,假如你的數(shù)據(jù)流量不是很大,采用隊列緩沖區(qū)的好處還是很明顯的:邏輯清晰、代碼簡單、維護方便。比較符合KISS原則。
??? ★進程方式
??? 說完了線程的方式,再來介紹基于進程的并發(fā)。
??? 跨進程的生產(chǎn)者/消費者模式,非常依賴于具體的進程間通訊(IPC)方式。而IPC的種類名目繁多,不便于挨個列舉(畢竟口水有限)。因此咱們挑選幾種跨平臺、且編程語言支持較多的IPC方式來說事兒。
??? ◇匿名管道
??? 感覺管道是最像隊列的IPC類型。生產(chǎn)者進程在管道的寫端放入數(shù)據(jù);消費者進程在管道的讀端取出數(shù)據(jù)。整個的效果和線程中使用隊列非常類似,區(qū)別在于使用管道就無需操心線程安全、內存分配等瑣事(操作系統(tǒng)暗中都幫你搞定了)。
??? 管道又分命名管道和匿名管道兩種,今天主要聊匿名管道。因為命名管道在不同的操作系統(tǒng)下差異較大(比如Win32和POSIX,在命名管道的API接口和功能實現(xiàn)上都有較大差異;有些平臺不支持命名管道,比如Windows CE)。除了操作系統(tǒng)的問題,對于有些編程語言(比如Java)來說,命名管道是無法使用的。所以我一般不推薦使用這玩意兒。
??? 其實匿名管道在不同平臺上的API接口,也是有差異的(比如Win32的CreatePipe和POSIX的pipe,用法就很不一樣)。但是我們可以僅使用標準輸入和標準輸出(以下簡稱stdio)來進行數(shù)據(jù)的流入流出。然后利用shell的管道符把生產(chǎn)者進程和消費者進程關聯(lián)起來(沒聽說過這種手法的同學,可以看"這里")。實際上,很多操作系統(tǒng)(尤其是POSIX風格的)自帶的命令都充分利用了這個特性來實現(xiàn)數(shù)據(jù)的傳輸(比如more、grep等)。
??? 這么干有幾個好處:
??? 1、基本上所有操作系統(tǒng)都支持在shell方式下使用管道符。因此很容易實現(xiàn)跨平臺。
??? 2、大部分編程語言都能夠操作stdio,因此跨編程語言也就容易實現(xiàn)。
??? 3、剛才已經(jīng)提到,管道方式省卻了線程安全方面的瑣事。有利于降低開發(fā)、調試成本。
??? 當然,這種方式也有自身的缺點:
??? 1、生產(chǎn)者進程和消費者進程必須得在同一臺主機上,無法跨機器通訊。這個缺點比較明顯。
??? 2、在一對一的情況下,這種方式挺合用。但如果要擴展到一對多或者多對一,那就有點棘手了。所以這種方式的擴展性要打個折扣。假如今后要考慮類似的擴展,這個缺點就比較明顯。
??? 3、由于管道是shell創(chuàng)建的,對于兩邊的進程不可見(程序看到的只是stdio)。在某些情況下,導致程序不便于對管道進行操縱(比如調整管道緩沖區(qū)尺寸)。這個缺點不太明顯。
??? 4、最后,這種方式只能單向傳數(shù)據(jù)。好在大多數(shù)情況下,消費者進程不需要傳數(shù)據(jù)給生產(chǎn)者進程。萬一你確實需要信息反饋(從消費者到生產(chǎn)者),那就費勁了。可能得考慮換種IPC方式。
??? 順便補充幾個注意事項,大伙兒留意一下:
??? 1、對stdio進行讀寫操作是以阻塞方式進行。比如管道中沒有數(shù)據(jù),消費者進程的讀操作就會一直停在哪兒,直到管道中重新有數(shù)據(jù)。
??? 2、由于stdio內部帶有自己的緩沖區(qū)(這緩沖區(qū)和管道緩沖區(qū)是兩碼事),有時會導致一些不太爽的現(xiàn)象(比如生產(chǎn)者進程輸出了數(shù)據(jù),但消費者進程沒有立即讀到)。具體的細節(jié),大伙兒可以看"這里"。
??? ◇SOCKET(TCP方式)
??? 基于TCP方式的SOCKET通訊是又一個類似于隊列的IPC方式。它同樣保證了數(shù)據(jù)的順序到達;同樣有緩沖的機制。而且這玩意兒也是跨平臺和跨語言的,和剛才介紹的shell管道符方式類似。
??? SOCKET相比shell管道符的方式,有啥優(yōu)點捏?主要有如下幾個優(yōu)點:
??? 1、SOCKET方式可以跨機器(便于實現(xiàn)分布式)。這是主要優(yōu)點。
??? 2、SOCKET方式便于將來擴展成為多對一或者一對多。這也是主要優(yōu)點。
??? 3、SOCKET可以設置阻塞和非阻塞方法,用起來比較靈活。這是次要優(yōu)點。
??? 4、SOCKET支持雙向通訊,有利于消費者反饋信息。
??? 當然有利就有弊。相對于上述shell管道的方式,使用SOCKET在編程上會更復雜一些。好在前人已經(jīng)做了大量的工作,搞出很多SOCKET通訊庫和框架給大伙兒用(比如C++的ACE庫、Python的Twisted)。借助于這些第三方的庫和框架,SOCKET方式用起來還是比較爽的。由于具體的網(wǎng)絡通訊庫該怎么用不是本系列的重點,此處就不細說了。
??? 雖然TCP在很多方面比UDP可靠,但鑒于跨機器通訊先天的不可預料性(比如網(wǎng)線可能被某傻X給拔錯了,網(wǎng)絡的忙閑波動可能很大),在程序設計上我們還是要多留一手。具體該如何做捏?可以在生產(chǎn)者進程和消費者進程內部各自再引入基于線程的"生產(chǎn)者/消費者模式"。這話聽著像繞口令,為了便于理解,畫張圖給大伙兒瞅一瞅。
??? 這么做的關鍵點在于把代碼分為兩部分:生產(chǎn)線程和消費線程屬于和業(yè)務邏輯相關的代碼(和通訊邏輯無關);發(fā)送線程和接收線程屬于通訊相關的代碼(和業(yè)務邏輯無關)。
??? 這樣的好處是很明顯的,具體如下:
??? 1、能夠應對暫時性的網(wǎng)絡故障。并且在網(wǎng)絡故障解除后,能夠繼續(xù)工作。
??? 2、網(wǎng)絡故障的應對處理方式(比如斷開后的嘗試重連),只影響發(fā)送和接收線程,不會影響生產(chǎn)線程和消費線程(業(yè)務邏輯部分)。
??? 3、具體的SOCKET方式(阻塞和非阻塞)只影響發(fā)送和接收線程,不影響生產(chǎn)線程和消費線程(業(yè)務邏輯部分)。
??? 4、不依賴TCP自身的發(fā)送緩沖區(qū)和接收緩沖區(qū)。(默認的TCP緩沖區(qū)的大小可能無法滿足實際要求)
??? 5、業(yè)務邏輯的變化(比如業(yè)務需求變更)不影響發(fā)送線程和接收線程。
??? 針對上述的最后一條,再多啰嗦幾句。如果整個業(yè)務系統(tǒng)中有多個進程是采用上述的模式,那或許可以重構一把:在業(yè)務邏輯代碼和通訊邏輯代碼之間切一刀,把業(yè)務邏輯無關的部分封裝成一個通訊中間件(說中間件顯得比較牛X :-)。如果大伙兒對這玩意兒有興趣,以后專門開個帖子聊。
??? [3]:環(huán)形緩沖區(qū)
??? 前一個帖子提及了隊列緩沖區(qū)可能存在的性能問題及解決方法:環(huán)形緩沖區(qū)。今天就專門來描述一下這個話題。
??? 為了防止有人給咱扣上“過度設計”的大帽子,事先聲明一下:只有當存儲空間的分配/釋放非常頻繁并且確實產(chǎn)生了明顯的影響,你才應該考慮環(huán)形緩沖區(qū)的使用。否則的話,還是老老實實用最基本、最簡單的隊列緩沖區(qū)吧。還有一點需要說明一下:本文所提及的“存儲空間”,不僅包括內存,還可能包括諸如硬盤之類的存儲介質。
??? ★環(huán)形緩沖區(qū) vs 隊列緩沖區(qū)
??? ◇外部接口相似
??? 在介紹環(huán)形緩沖區(qū)之前,咱們先來回顧一下普通的隊列。普通的隊列有一個寫入端和一個讀出端。隊列為空的時候,讀出端無法讀取數(shù)據(jù);當隊列滿(達到最大尺寸)時,寫入端無法寫入數(shù)據(jù)。
??? 對于使用者來講,環(huán)形緩沖區(qū)和隊列緩沖區(qū)是一樣的。它也有一個寫入端(用于push)和一個讀出端(用于pop),也有緩沖區(qū)“滿”和“空”的狀態(tài)。所以,從隊列緩沖區(qū)切換到環(huán)形緩沖區(qū),對于使用者來說能比較平滑地過渡。
??? ◇內部結構迥異
??? 雖然兩者的對外接口差不多,但是內部結構和運作機制有很大差別。隊列的內部結構此處就不多啰嗦了。重點介紹一下環(huán)形緩沖區(qū)的內部結構。
??? 大伙兒可以把環(huán)形緩沖區(qū)的讀出端(以下簡稱R)和寫入端(以下簡稱W)想象成是兩個人在體育場跑道上追逐(R追W)。當R追上W的時候,就是緩沖區(qū)為空;當W追上R的時候(W比R多跑一圈),就是緩沖區(qū)滿。
??? 為了形象起見,去找來一張圖并略作修改,如下:
??? 從上圖可以看出,環(huán)形緩沖區(qū)所有的push和pop操作都是在一個固定的存儲空間內進行。而隊列緩沖區(qū)在push的時候,可能會分配存儲空間用于存儲新元素;在pop時,可能會釋放廢棄元素的存儲空間。所以環(huán)形方式相比隊列方式,少掉了對于緩沖區(qū)元素所用存儲空間的分配、釋放。這是環(huán)形緩沖區(qū)的一個主要優(yōu)勢。
??? ★環(huán)形緩沖區(qū)的實現(xiàn)
??? 如果你手頭已經(jīng)有現(xiàn)成的環(huán)形緩沖區(qū)可供使用,并且你對環(huán)形緩沖區(qū)的內部實現(xiàn)不感興趣,可以跳過這段。
??? ◇數(shù)組方式 vs 鏈表方式
??? 環(huán)形緩沖區(qū)的內部實現(xiàn),即可基于數(shù)組(此處的數(shù)組,泛指連續(xù)存儲空間)實現(xiàn),也可基于鏈表實現(xiàn)。
??? 數(shù)組在物理存儲上是一維的連續(xù)線性結構,可以在初始化時,把存儲空間一次性分配好,這是數(shù)組方式的優(yōu)點。但是要使用數(shù)組來模擬環(huán),你必須在邏輯上把數(shù)組的頭和尾相連。在順序遍歷數(shù)組時,對尾部元素(最后一個元素)要作一下特殊處理。訪問尾部元素的下一個元素時,要重新回到頭部元素(第0個元素)。如下圖所示:
??? 使用鏈表的方式,正好和數(shù)組相反:鏈表省去了頭尾相連的特殊處理。但是鏈表在初始化的時候比較繁瑣,而且在有些場合(比如后面提到的跨進程的IPC)不太方便使用。
??? ◇讀寫操作
??? 環(huán)形緩沖區(qū)要維護兩個索引,分別對應寫入端(W)和讀取端(R)。寫入(push)的時候,先確保環(huán)沒滿,然后把數(shù)據(jù)復制到W所對應的元素,最后W指向下一個元素;讀取(pop)的時候,先確保環(huán)沒空,然后返回R對應的元素,最后R指向下一個元素。
??? ◇判斷“空”和“滿”
??? 上述的操作并不復雜,不過有一個小小的麻煩:空環(huán)和滿環(huán)的時候,R和W都指向同一個位置!這樣就無法判斷到底是“空”還是“滿”。大體上有兩種方法可以解決該問題。
??? 辦法1:始終保持一個元素不用
??? 當空環(huán)的時候,R和W重疊。當W比R跑得快,追到距離R還有一個元素間隔的時候,就認為環(huán)已經(jīng)滿。當環(huán)內元素占用的存儲空間較大的時候,這種辦法顯得很土(浪費空間)。
??? 辦法2:維護額外變量
??? 如果不喜歡上述辦法,還可以采用額外的變量來解決。比如可以用一個整數(shù)記錄當前環(huán)中已經(jīng)保存的元素個數(shù)(該整數(shù)>=0)。當R和W重疊的時候,通過該變量就可以知道是“空”還是“滿”。
??? ◇元素的存儲
??? 由于環(huán)形緩沖區(qū)本身就是要降低存儲空間分配的開銷,因此緩沖區(qū)中元素的類型要選好。盡量存儲值類型的數(shù)據(jù),而不要存儲指針(引用)類型的數(shù)據(jù)。因為指針類型的數(shù)據(jù)又會引起存儲空間(比如堆內存)的分配和釋放,使得環(huán)形緩沖區(qū)的效果打折扣。
??? ★應用場合
??? 剛才介紹了環(huán)形緩沖區(qū)內部的實現(xiàn)機制。按照前一個帖子的慣例,我們來介紹一下在線程和進程方式下的使用。
??? 如果你所使用的編程語言和開發(fā)庫中帶有現(xiàn)成的、成熟的環(huán)形緩沖區(qū),強烈建議使用現(xiàn)成的庫,不要重新制造輪子;確實找不到現(xiàn)成的,才考慮自己實現(xiàn)。如果你純粹是業(yè)余時間練練手,那另當別論。
??? ◇用于并發(fā)線程
??? 和線程中的隊列緩沖區(qū)類似,線程中的環(huán)形緩沖區(qū)也要考慮線程安全的問題。除非你使用的環(huán)形緩沖區(qū)的庫已經(jīng)幫你實現(xiàn)了線程安全,否則你還是得自己動手搞定。線程方式下的環(huán)形緩沖區(qū)用得比較多,相關的網(wǎng)上資料也多,下面就大致介紹幾個。
??? 對于C++的程序員,強烈推薦使用boost提供的circular_buffer模板,該模板最開始是在boost 1.35版本中引入的。鑒于boost在C++社區(qū)中的地位,大伙兒應該可以放心使用該模板。
??? 對于C程序員,可以去看看開源項目circbuf,不過該項目是GPL協(xié)議的,不太爽;而且活躍度不太高;而且只有一個開發(fā)人員。大伙兒慎用!建議只拿它當參考。
??? 對于C#程序員,可以參考CodeProject上的一個示例。
??? ◇用于并發(fā)進程
??? 進程間的環(huán)形緩沖區(qū),似乎少有現(xiàn)成的庫可用。大伙兒只好自己動手、豐衣足食了。
??? 適用于進程間環(huán)形緩沖的IPC類型,常見的有共享內存和文件。在這兩種方式上進行環(huán)形緩沖,通常都采用數(shù)組的方式實現(xiàn)。程序事先分配好一個固定長度的存儲空間,然后具體的讀寫操作、判斷“空”和“滿”、元素存儲等細節(jié)就可參照前面所說的來進行。
??? 共享內存方式的性能很好,適用于數(shù)據(jù)流量很大的場景。但是有些語言(比如Java)對于共享內存不支持。因此,該方式在多語言協(xié)同開發(fā)的系統(tǒng)中,會有一定的局限性。
??? 而文件方式在編程語言方面支持很好,幾乎所有編程語言都支持操作文件。但它可能會受限于磁盤讀寫(Disk I/O)的性能。所以文件方式不太適合于快速數(shù)據(jù)傳輸;但是對于某些“數(shù)據(jù)單元”很大的場合,文件方式是值得考慮的。
??? 對于進程間的環(huán)形緩沖區(qū),同樣要考慮好進程間的同步、互斥等問題,限于篇幅,此處就不細說了。
轉自:架構設計:生產(chǎn)者/消費者模式_軟件學園_科技時代_新浪網(wǎng)
總結
以上是生活随笔為你收集整理的生产者消费者模式详解(转载)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 规范的参考文献格式
- 下一篇: android Sensor 驱动编写-