[0]:概述
今天打算來(lái)介紹一下“生產(chǎn)者/消費(fèi)者模式”,這玩意兒在很多開(kāi)發(fā)領(lǐng)域都能派上用場(chǎng)。由于該模式很重要,打算分幾個(gè)帖子來(lái)介紹。今天這個(gè)帖子先來(lái)掃盲一把。如果你對(duì)這個(gè)模式已經(jīng)比較了解,請(qǐng)?zhí)^(guò)本掃盲帖,直接看下一個(gè)帖子(關(guān)于該模式的具體應(yīng)用)。
看到這里,可能有同學(xué)心中犯嘀咕了:在四人幫(GOF)的23種模式里面似乎沒(méi)聽(tīng)說(shuō)過(guò)這種嘛!其實(shí)GOF那經(jīng)典的23種模式主要是基于OO的(從書名《Design Patterns: Elements of Reusable Object-Oriented Software》就可以看出來(lái))。而Pattern實(shí)際上即可以是OO的Pattern,也可以是非OO的Pattern的。
★簡(jiǎn)介
言歸正傳!在實(shí)際的軟件開(kāi)發(fā)過(guò)程中,經(jīng)常會(huì)碰到如下場(chǎng)景:某個(gè)模塊負(fù)責(zé)產(chǎn)生數(shù)據(jù),這些數(shù)據(jù)由另一個(gè)模塊來(lái)負(fù)責(zé)處理(此處的模塊是廣義的,可以是類、函數(shù)、線程、進(jìn)程等)。產(chǎn)生數(shù)據(jù)的模塊,就形象地稱為生產(chǎn)者;而處理數(shù)據(jù)的模塊,就稱為消費(fèi)者。
單單抽象出生產(chǎn)者和消費(fèi)者,還夠不上是生產(chǎn)者/消費(fèi)者模式。該模式還需要有一個(gè)緩沖區(qū)處于生產(chǎn)者和消費(fèi)者之間,作為一個(gè)中介。生產(chǎn)者把數(shù)據(jù)放入緩沖區(qū),而消費(fèi)者從緩沖區(qū)取出數(shù)據(jù)。大概的結(jié)構(gòu)如下圖。

為了不至于太抽象,我們舉一個(gè)寄信的例子(雖說(shuō)這年頭寄信已經(jīng)不時(shí)興,但這個(gè)例子還是比較貼切的)。假設(shè)你要寄一封平信,大致過(guò)程如下:
1、你把信寫好——相當(dāng)于生產(chǎn)者制造數(shù)據(jù)
2、你把信放入郵筒——相當(dāng)于生產(chǎn)者把數(shù)據(jù)放入緩沖區(qū)
3、郵遞員把信從郵筒取出——相當(dāng)于消費(fèi)者把數(shù)據(jù)取出緩沖區(qū)
4、郵遞員把信拿去郵局做相應(yīng)的處理——相當(dāng)于消費(fèi)者處理數(shù)據(jù)
★優(yōu)點(diǎn)
可能有同學(xué)會(huì)問(wèn)了:這個(gè)緩沖區(qū)有什么用捏?為什么不讓生產(chǎn)者直接調(diào)用消費(fèi)者的某個(gè)函數(shù),直接把數(shù)據(jù)傳遞過(guò)去?搞出這么一個(gè)緩沖區(qū)作甚?
其實(shí)這里面是大有講究的,大概有如下一些好處。
◇解耦
假設(shè)生產(chǎn)者和消費(fèi)者分別是兩個(gè)類。如果讓生產(chǎn)者直接調(diào)用消費(fèi)者的某個(gè)方法,那么生產(chǎn)者對(duì)于消費(fèi)者就會(huì)產(chǎn)生依賴(也就是耦合)。將來(lái)如果消費(fèi)者的代碼發(fā)生變化,可能會(huì)影響到生產(chǎn)者。而如果兩者都依賴于某個(gè)緩沖區(qū),兩者之間不直接依賴,耦合也就相應(yīng)降低了。
接著上述的例子,如果不使用郵筒(也就是緩沖區(qū)),你必須得把信直接交給郵遞員。有同學(xué)會(huì)說(shuō),直接給郵遞員不是挺簡(jiǎn)單的嘛?其實(shí)不簡(jiǎn)單,你必須得認(rèn)識(shí)誰(shuí)是郵遞員,才能把信給他(光憑身上穿的制服,萬(wàn)一有人假冒,就慘了)。這就產(chǎn)生和你和郵遞員之間的依賴(相當(dāng)于生產(chǎn)者和消費(fèi)者的強(qiáng)耦合)。萬(wàn)一哪天郵遞員換人了,你還要重新認(rèn)識(shí)一下(相當(dāng)于消費(fèi)者變化導(dǎo)致修改生產(chǎn)者代碼)。而郵筒相對(duì)來(lái)說(shuō)比較固定,你依賴它的成本就比較低(相當(dāng)于和緩沖區(qū)之間的弱耦合)。
◇支持并發(fā)(concurrency)
生產(chǎn)者直接調(diào)用消費(fèi)者的某個(gè)方法,還有另一個(gè)弊端。由于函數(shù)調(diào)用是同步的(或者叫阻塞的),在消費(fèi)者的方法沒(méi)有返回之前,生產(chǎn)者只好一直等在那邊。萬(wàn)一消費(fèi)者處理數(shù)據(jù)很慢,生產(chǎn)者就會(huì)白白糟蹋大好時(shí)光。
使用了生產(chǎn)者/消費(fèi)者模式之后,生產(chǎn)者和消費(fèi)者可以是兩個(gè)獨(dú)立的并發(fā)主體(常見(jiàn)并發(fā)類型有進(jìn)程和線程兩種,后面的帖子會(huì)講兩種并發(fā)類型下的應(yīng)用)。生產(chǎn)者把制造出來(lái)的數(shù)據(jù)往緩沖區(qū)一丟,就可以再去生產(chǎn)下一個(gè)數(shù)據(jù)。基本上不用依賴消費(fèi)者的處理速度。
其實(shí)當(dāng)初這個(gè)模式,主要就是用來(lái)處理并發(fā)問(wèn)題的。
從寄信的例子來(lái)看。如果沒(méi)有郵筒,你得拿著信傻站在路口等郵遞員過(guò)來(lái)收(相當(dāng)于生產(chǎn)者阻塞);又或者郵遞員得挨家挨戶問(wèn),誰(shuí)要寄信(相當(dāng)于消費(fèi)者輪詢)。不管是哪種方法,都挺土的。
◇支持忙閑不均
緩沖區(qū)還有另一個(gè)好處。如果制造數(shù)據(jù)的速度時(shí)快時(shí)慢,緩沖區(qū)的好處就體現(xiàn)出來(lái)了。當(dāng)數(shù)據(jù)制造快的時(shí)候,消費(fèi)者來(lái)不及處理,未處理的數(shù)據(jù)可以暫時(shí)存在緩沖區(qū)中。等生產(chǎn)者的制造速度慢下來(lái),消費(fèi)者再慢慢處理掉。
為了充分復(fù)用,我們?cè)倌眉男诺睦觼?lái)說(shuō)事。假設(shè)郵遞員一次只能帶走1000封信。萬(wàn)一某次碰上情人節(jié)(也可能是圣誕節(jié))送賀卡,需要寄出去的信超過(guò)1000封,這時(shí)候郵筒這個(gè)緩沖區(qū)就派上用場(chǎng)了。郵遞員把來(lái)不及帶走的信暫存在郵筒中,等下次過(guò)來(lái)時(shí)再拿走。
費(fèi)了這么多口水,希望原先不太了解生產(chǎn)者/消費(fèi)者模式的同學(xué)能夠明白它是怎么一回事。然后在下一個(gè)帖子中,我們來(lái)說(shuō)說(shuō)如何確定數(shù)據(jù)單元。
另外,為了方便閱讀,把本系列帖子的目錄整理如下:
1、如何確定數(shù)據(jù)單元
2、隊(duì)列緩沖區(qū)
3、隊(duì)列緩沖區(qū)
4、雙緩沖區(qū)
5、......
[1]:如何確定數(shù)據(jù)單元?
既然前一個(gè)帖子已經(jīng)搞過(guò)掃盲了,那接下來(lái)應(yīng)該開(kāi)始聊一些具體的編程技術(shù)問(wèn)題了。不過(guò)在進(jìn)入具體的技術(shù)細(xì)節(jié)之前,咱們先要搞明白一個(gè)問(wèn)題:如何確定數(shù)據(jù)單元?只有把數(shù)據(jù)單元分析清楚,后面的技術(shù)設(shè)計(jì)才好搞。
★啥是數(shù)據(jù)單元
何謂數(shù)據(jù)單元捏?簡(jiǎn)單地說(shuō),每次生產(chǎn)者放到緩沖區(qū)的,就是一個(gè)數(shù)據(jù)單元;每次消費(fèi)者從緩沖區(qū)取出的,也是一個(gè)數(shù)據(jù)單元。對(duì)于前一個(gè)帖子中寄信的例子,我們可以把每一封單獨(dú)的信件看成是一個(gè)數(shù)據(jù)單元。
不過(guò)光這么介紹,太過(guò)于簡(jiǎn)單,無(wú)助于大伙兒分析出這玩意兒。所以,后面咱們來(lái)看一下數(shù)據(jù)單元需要具備哪些特性。搞明白這些特性之后,就容易從復(fù)雜的業(yè)務(wù)邏輯中分析出適合做數(shù)據(jù)單元的東西了。
★數(shù)據(jù)單元的特性
分析數(shù)據(jù)單元,需要考慮如下幾個(gè)方面的特性:
◇關(guān)聯(lián)到業(yè)務(wù)對(duì)象
首先,數(shù)據(jù)單元必須關(guān)聯(lián)到某種業(yè)務(wù)對(duì)象。在考慮該問(wèn)題的時(shí)候,你必須深刻理解當(dāng)前這個(gè)生產(chǎn)者/消費(fèi)者模式所對(duì)應(yīng)的業(yè)務(wù)邏輯,才能夠作出合適的判斷。
由于“寄信”這個(gè)業(yè)務(wù)邏輯比較簡(jiǎn)單,所以大伙兒很容易就可以判斷出數(shù)據(jù)單元是啥。但現(xiàn)實(shí)生活中,往往沒(méi)這么樂(lè)觀。大多數(shù)業(yè)務(wù)邏輯都比較復(fù)雜,當(dāng)中包含的業(yè)務(wù)對(duì)象是層次繁多、類型各異。在這種情況下,就不易作出決策了。
這一步很重要,如果選錯(cuò)了業(yè)務(wù)對(duì)象,會(huì)導(dǎo)致后續(xù)程序設(shè)計(jì)和編碼實(shí)現(xiàn)的復(fù)雜度大為上升,增加了開(kāi)發(fā)和維護(hù)成本。
◇完整性
所謂完整性,就是在傳輸過(guò)程中,要保證該數(shù)據(jù)單元的完整。要么整個(gè)數(shù)據(jù)單元被傳遞到消費(fèi)者,要么完全沒(méi)有傳遞到消費(fèi)者。不允許出現(xiàn)部分傳遞的情形。
對(duì)于寄信來(lái)說(shuō),你不能把半封信放入郵筒;同樣的,郵遞員從郵筒中拿信,也不能只拿出信的一部分。
◇獨(dú)立性
所謂獨(dú)立性,就是各個(gè)數(shù)據(jù)單元之間沒(méi)有互相依賴,某個(gè)數(shù)據(jù)單元傳輸失敗不應(yīng)該影響已經(jīng)完成傳輸?shù)膯卧?;也不?yīng)該影響尚未傳輸?shù)膯卧?/p>
為啥會(huì)出現(xiàn)傳輸失敗捏?假如生產(chǎn)者的生產(chǎn)速度在一段時(shí)間內(nèi)一直超過(guò)消費(fèi)者的處理速度,那就會(huì)導(dǎo)致緩沖區(qū)不斷增長(zhǎng)并達(dá)到上限,之后的數(shù)據(jù)單元就會(huì)被丟棄。如果數(shù)據(jù)單元相互獨(dú)立,等到生產(chǎn)者的速度降下來(lái)之后,后續(xù)的數(shù)據(jù)單元繼續(xù)處理,不會(huì)受到牽連;反之,如果數(shù)據(jù)單元之間有某種耦合,導(dǎo)致被丟棄的數(shù)據(jù)單元會(huì)影響到后續(xù)其它單元的處理,那就會(huì)使程序邏輯變得非常復(fù)雜。
對(duì)于寄信來(lái)說(shuō),某封信弄丟了,不會(huì)影響后續(xù)信件的送達(dá);當(dāng)然更不會(huì)影響已經(jīng)送達(dá)的信件。
◇顆粒度
前面提到,數(shù)據(jù)單元需要關(guān)聯(lián)到某種業(yè)務(wù)對(duì)象。那么數(shù)據(jù)單元和業(yè)務(wù)對(duì)象是否要一一對(duì)應(yīng)捏?很多場(chǎng)合確實(shí)是一一對(duì)應(yīng)的。
不過(guò),有時(shí)出于性能等因素的考慮,也可能會(huì)把N個(gè)業(yè)務(wù)對(duì)象打包成一個(gè)數(shù)據(jù)單元。那么,這個(gè)N該如何取值就是顆粒度的考慮了。顆粒度的大小是有講究的。太大的顆粒度可能會(huì)造成某種浪費(fèi);太小的顆粒度可能會(huì)造成性能問(wèn)題。顆粒度的權(quán)衡要基于多方面的因素,以及一些經(jīng)驗(yàn)值的考量。
還是拿寄信的例子。如果顆粒度過(guò)?。ū热缭O(shè)定為1),那郵遞員每次只取出1封信。如果信件多了,那就得來(lái)回跑好多趟,浪費(fèi)了時(shí)間。
如果顆粒度太大(比如設(shè)定為100),那寄信的人得等到湊滿100封信才拿去放入郵筒。假如平時(shí)很少寫信,就得等上很久,也不太爽。
可能有同學(xué)會(huì)問(wèn):生產(chǎn)者和消費(fèi)者的顆粒度能否設(shè)置成不同大?。ū热鐚?duì)于寄信人設(shè)置成1,對(duì)于郵遞員設(shè)置成100)。當(dāng)然,理論上可以這么干,但是在某些情況下會(huì)增加程序邏輯和代碼實(shí)現(xiàn)的復(fù)雜度。后面討論具體技術(shù)細(xì)節(jié)時(shí),或許會(huì)聊到這個(gè)問(wèn)題。
好,數(shù)據(jù)單元的話題就說(shuō)到這。希望通過(guò)本帖子,大伙兒能夠搞明白數(shù)據(jù)單元到底是怎么一回事。下一個(gè)帖子,咱們來(lái)聊一下“基于隊(duì)列的緩沖區(qū)”,技術(shù)上如何實(shí)現(xiàn)。
[2]:隊(duì)列緩沖區(qū)
經(jīng)過(guò)前面兩個(gè)帖子的鋪墊,今天終于開(kāi)始聊一些具體的編程技術(shù)了。由于不同的緩沖區(qū)類型、不同的并發(fā)場(chǎng)景對(duì)于具體的技術(shù)實(shí)現(xiàn)有較大的影響。為了深入淺出、便于大伙兒理解,咱們先來(lái)介紹最傳統(tǒng)、最常見(jiàn)的方式。也就是單個(gè)生產(chǎn)者對(duì)應(yīng)單個(gè)消費(fèi)者,當(dāng)中用隊(duì)列(FIFO)作緩沖。
關(guān)于并發(fā)的場(chǎng)景,在之前的帖子“進(jìn)程還線程?是一個(gè)問(wèn)題!”中,已經(jīng)專門論述了進(jìn)程和線程各自的優(yōu)缺點(diǎn),兩者皆不可偏廢。所以,后面對(duì)各種緩沖區(qū)類型的介紹都會(huì)同時(shí)提及進(jìn)程方式和線程方式。
★線程方式
先來(lái)說(shuō)一下并發(fā)線程中使用隊(duì)列的例子,以及相關(guān)的優(yōu)缺點(diǎn)。
◇內(nèi)存分配的性能
在線程方式下,生產(chǎn)者和消費(fèi)者各自是一個(gè)線程。生產(chǎn)者把數(shù)據(jù)寫入隊(duì)列頭(以下簡(jiǎn)稱push),消費(fèi)者從隊(duì)列尾部讀出數(shù)據(jù)(以下簡(jiǎn)稱pop)。當(dāng)隊(duì)列為空,消費(fèi)者就稍息(稍事休息);當(dāng)隊(duì)列滿(達(dá)到最大長(zhǎng)度),生產(chǎn)者就稍息。整個(gè)流程并不復(fù)雜。
那么,上述過(guò)程會(huì)有什么問(wèn)題捏?一個(gè)主要的問(wèn)題是關(guān)于內(nèi)存分配的性能開(kāi)銷。對(duì)于常見(jiàn)的隊(duì)列實(shí)現(xiàn):在每次push時(shí),可能涉及到堆內(nèi)存的分配;在每次pop時(shí),可能涉及堆內(nèi)存的釋放。假如生產(chǎn)者和消費(fèi)者都很勤快,頻繁地push、pop,那內(nèi)存分配的開(kāi)銷就很可觀了。對(duì)于內(nèi)存分配的開(kāi)銷,用Java的同學(xué)可以參見(jiàn)前幾天的帖子“Java性能優(yōu)化[1]”;對(duì)于用C/C++的同學(xué),想必對(duì)OS底層機(jī)制會(huì)更清楚,應(yīng)該知道分配堆內(nèi)存(new或malloc)會(huì)有加鎖的開(kāi)銷和用戶態(tài)/核心態(tài)切換的開(kāi)銷。
那該怎么辦捏?請(qǐng)聽(tīng)下文分解,關(guān)于“生產(chǎn)者/消費(fèi)者模式[3]:環(huán)形緩沖區(qū)”。
◇同步和互斥的性能
另外,由于兩個(gè)線程共用一個(gè)隊(duì)列,自然就會(huì)涉及到線程間諸如同步啊、互斥啊、死鎖啊等等勞心費(fèi)神的事情。好在"操作系統(tǒng)"這門課程對(duì)此有詳細(xì)介紹,學(xué)過(guò)的同學(xué)應(yīng)該還有點(diǎn)印象吧?對(duì)于沒(méi)學(xué)過(guò)這門課的同學(xué),也不必難過(guò),網(wǎng)上相關(guān)的介紹挺多的(比如"這里"),大伙自己去瞅一瞅。關(guān)于這方面的細(xì)節(jié),咱今天就不多啰嗦了。
這會(huì)兒要細(xì)談的是,同步和互斥的性能開(kāi)銷。在很多場(chǎng)合中,諸如信號(hào)量、互斥量等玩意兒的使用也是有不小的開(kāi)銷的(某些情況下,也可能導(dǎo)致用戶態(tài)/核心態(tài)切換)。如果像剛才所說(shuō),生產(chǎn)者和消費(fèi)者都很勤快,那這些開(kāi)銷也不容小覷啊。
這又該咋辦捏?請(qǐng)聽(tīng)下文的下文分解,關(guān)于“生產(chǎn)者/消費(fèi)者模式[4]:雙緩沖區(qū)”。
◇適用于隊(duì)列的場(chǎng)合
剛才盡批判了隊(duì)列的缺點(diǎn),難道隊(duì)列方式就一無(wú)是處?非也。由于隊(duì)列是很常見(jiàn)的數(shù)據(jù)結(jié)構(gòu),大部分編程語(yǔ)言都內(nèi)置了隊(duì)列的支持(具體介紹見(jiàn)"這里"),有些語(yǔ)言甚至提供了線程安全的隊(duì)列(比如JDK 1.5引入的ArrayBlockingQueue)。因此,開(kāi)發(fā)人員可以撿現(xiàn)成,避免了重新發(fā)明輪子。
所以,假如你的數(shù)據(jù)流量不是很大,采用隊(duì)列緩沖區(qū)的好處還是很明顯的:邏輯清晰、代碼簡(jiǎn)單、維護(hù)方便。比較符合KISS原則。
★進(jìn)程方式
說(shuō)完了線程的方式,再來(lái)介紹基于進(jìn)程的并發(fā)。
跨進(jìn)程的生產(chǎn)者/消費(fèi)者模式,非常依賴于具體的進(jìn)程間通訊(IPC)方式。而IPC的種類名目繁多,不便于挨個(gè)列舉(畢竟口水有限)。因此咱們挑選幾種跨平臺(tái)、且編程語(yǔ)言支持較多的IPC方式來(lái)說(shuō)事兒。
◇匿名管道
感覺(jué)管道是最像隊(duì)列的IPC類型。生產(chǎn)者進(jìn)程在管道的寫端放入數(shù)據(jù);消費(fèi)者進(jìn)程在管道的讀端取出數(shù)據(jù)。整個(gè)的效果和線程中使用隊(duì)列非常類似,區(qū)別在于使用管道就無(wú)需操心線程安全、內(nèi)存分配等瑣事(操作系統(tǒng)暗中都幫你搞定了)。
管道又分命名管道和匿名管道兩種,今天主要聊匿名管道。因?yàn)槊艿涝诓煌牟僮飨到y(tǒng)下差異較大(比如Win32和POSIX,在命名管道的API接口和功能實(shí)現(xiàn)上都有較大差異;有些平臺(tái)不支持命名管道,比如Windows CE)。除了操作系統(tǒng)的問(wèn)題,對(duì)于有些編程語(yǔ)言(比如Java)來(lái)說(shuō),命名管道是無(wú)法使用的。所以我一般不推薦使用這玩意兒。
其實(shí)匿名管道在不同平臺(tái)上的API接口,也是有差異的(比如Win32的CreatePipe和POSIX的pipe,用法就很不一樣)。但是我們可以僅使用標(biāo)準(zhǔn)輸入和標(biāo)準(zhǔn)輸出(以下簡(jiǎn)稱stdio)來(lái)進(jìn)行數(shù)據(jù)的流入流出。然后利用shell的管道符把生產(chǎn)者進(jìn)程和消費(fèi)者進(jìn)程關(guān)聯(lián)起來(lái)(沒(méi)聽(tīng)說(shuō)過(guò)這種手法的同學(xué),可以看"這里")。實(shí)際上,很多操作系統(tǒng)(尤其是POSIX風(fēng)格的)自帶的命令都充分利用了這個(gè)特性來(lái)實(shí)現(xiàn)數(shù)據(jù)的傳輸(比如more、grep等)。
這么干有幾個(gè)好處:
1、基本上所有操作系統(tǒng)都支持在shell方式下使用管道符。因此很容易實(shí)現(xiàn)跨平臺(tái)。
2、大部分編程語(yǔ)言都能夠操作stdio,因此跨編程語(yǔ)言也就容易實(shí)現(xiàn)。
3、剛才已經(jīng)提到,管道方式省卻了線程安全方面的瑣事。有利于降低開(kāi)發(fā)、調(diào)試成本。
當(dāng)然,這種方式也有自身的缺點(diǎn):
1、生產(chǎn)者進(jìn)程和消費(fèi)者進(jìn)程必須得在同一臺(tái)主機(jī)上,無(wú)法跨機(jī)器通訊。這個(gè)缺點(diǎn)比較明顯。
2、在一對(duì)一的情況下,這種方式挺合用。但如果要擴(kuò)展到一對(duì)多或者多對(duì)一,那就有點(diǎn)棘手了。所以這種方式的擴(kuò)展性要打個(gè)折扣。假如今后要考慮類似的擴(kuò)展,這個(gè)缺點(diǎn)就比較明顯。
3、由于管道是shell創(chuàng)建的,對(duì)于兩邊的進(jìn)程不可見(jiàn)(程序看到的只是stdio)。在某些情況下,導(dǎo)致程序不便于對(duì)管道進(jìn)行操縱(比如調(diào)整管道緩沖區(qū)尺寸)。這個(gè)缺點(diǎn)不太明顯。
4、最后,這種方式只能單向傳數(shù)據(jù)。好在大多數(shù)情況下,消費(fèi)者進(jìn)程不需要傳數(shù)據(jù)給生產(chǎn)者進(jìn)程。萬(wàn)一你確實(shí)需要信息反饋(從消費(fèi)者到生產(chǎn)者),那就費(fèi)勁了。可能得考慮換種IPC方式。
順便補(bǔ)充幾個(gè)注意事項(xiàng),大伙兒留意一下:
1、對(duì)stdio進(jìn)行讀寫操作是以阻塞方式進(jìn)行。比如管道中沒(méi)有數(shù)據(jù),消費(fèi)者進(jìn)程的讀操作就會(huì)一直停在哪兒,直到管道中重新有數(shù)據(jù)。
2、由于stdio內(nèi)部帶有自己的緩沖區(qū)(這緩沖區(qū)和管道緩沖區(qū)是兩碼事),有時(shí)會(huì)導(dǎo)致一些不太爽的現(xiàn)象(比如生產(chǎn)者進(jìn)程輸出了數(shù)據(jù),但消費(fèi)者進(jìn)程沒(méi)有立即讀到)。具體的細(xì)節(jié),大伙兒可以看"這里"。
◇SOCKET(TCP方式)
基于TCP方式的SOCKET通訊是又一個(gè)類似于隊(duì)列的IPC方式。它同樣保證了數(shù)據(jù)的順序到達(dá);同樣有緩沖的機(jī)制。而且這玩意兒也是跨平臺(tái)和跨語(yǔ)言的,和剛才介紹的shell管道符方式類似。
SOCKET相比shell管道符的方式,有啥優(yōu)點(diǎn)捏?主要有如下幾個(gè)優(yōu)點(diǎn):
1、SOCKET方式可以跨機(jī)器(便于實(shí)現(xiàn)分布式)。這是主要優(yōu)點(diǎn)。
2、SOCKET方式便于將來(lái)擴(kuò)展成為多對(duì)一或者一對(duì)多。這也是主要優(yōu)點(diǎn)。
3、SOCKET可以設(shè)置阻塞和非阻塞方法,用起來(lái)比較靈活。這是次要優(yōu)點(diǎn)。
4、SOCKET支持雙向通訊,有利于消費(fèi)者反饋信息。
當(dāng)然有利就有弊。相對(duì)于上述shell管道的方式,使用SOCKET在編程上會(huì)更復(fù)雜一些。好在前人已經(jīng)做了大量的工作,搞出很多SOCKET通訊庫(kù)和框架給大伙兒用(比如C++的ACE庫(kù)、Python的Twisted)。借助于這些第三方的庫(kù)和框架,SOCKET方式用起來(lái)還是比較爽的。由于具體的網(wǎng)絡(luò)通訊庫(kù)該怎么用不是本系列的重點(diǎn),此處就不細(xì)說(shuō)了。
雖然TCP在很多方面比UDP可靠,但鑒于跨機(jī)器通訊先天的不可預(yù)料性(比如網(wǎng)線可能被某傻X給拔錯(cuò)了,網(wǎng)絡(luò)的忙閑波動(dòng)可能很大),在程序設(shè)計(jì)上我們還是要多留一手。具體該如何做捏?可以在生產(chǎn)者進(jìn)程和消費(fèi)者進(jìn)程內(nèi)部各自再引入基于線程的"生產(chǎn)者/消費(fèi)者模式"。這話聽(tīng)著像繞口令,為了便于理解,畫張圖給大伙兒瞅一瞅。

這么做的關(guān)鍵點(diǎn)在于把代碼分為兩部分:生產(chǎn)線程和消費(fèi)線程屬于和業(yè)務(wù)邏輯相關(guān)的代碼(和通訊邏輯無(wú)關(guān));發(fā)送線程和接收線程屬于通訊相關(guān)的代碼(和業(yè)務(wù)邏輯無(wú)關(guān))。
這樣的好處是很明顯的,具體如下:
1、能夠應(yīng)對(duì)暫時(shí)性的網(wǎng)絡(luò)故障。并且在網(wǎng)絡(luò)故障解除后,能夠繼續(xù)工作。
2、網(wǎng)絡(luò)故障的應(yīng)對(duì)處理方式(比如斷開(kāi)后的嘗試重連),只影響發(fā)送和接收線程,不會(huì)影響生產(chǎn)線程和消費(fèi)線程(業(yè)務(wù)邏輯部分)。
3、具體的SOCKET方式(阻塞和非阻塞)只影響發(fā)送和接收線程,不影響生產(chǎn)線程和消費(fèi)線程(業(yè)務(wù)邏輯部分)。
4、不依賴TCP自身的發(fā)送緩沖區(qū)和接收緩沖區(qū)。(默認(rèn)的TCP緩沖區(qū)的大小可能無(wú)法滿足實(shí)際要求)
5、業(yè)務(wù)邏輯的變化(比如業(yè)務(wù)需求變更)不影響發(fā)送線程和接收線程。
針對(duì)上述的最后一條,再多啰嗦幾句。如果整個(gè)業(yè)務(wù)系統(tǒng)中有多個(gè)進(jìn)程是采用上述的模式,那或許可以重構(gòu)一把:在業(yè)務(wù)邏輯代碼和通訊邏輯代碼之間切一刀,把業(yè)務(wù)邏輯無(wú)關(guān)的部分封裝成一個(gè)通訊中間件(說(shuō)中間件顯得比較牛X :-)。如果大伙兒對(duì)這玩意兒有興趣,以后專門開(kāi)個(gè)帖子聊。
[3]:環(huán)形緩沖區(qū)
前一個(gè)帖子提及了隊(duì)列緩沖區(qū)可能存在的性能問(wèn)題及解決方法:環(huán)形緩沖區(qū)。今天就專門來(lái)描述一下這個(gè)話題。
為了防止有人給咱扣上“過(guò)度設(shè)計(jì)”的大帽子,事先聲明一下:只有當(dāng)存儲(chǔ)空間的分配/釋放非常頻繁并且確實(shí)產(chǎn)生了明顯的影響,你才應(yīng)該考慮環(huán)形緩沖區(qū)的使用。否則的話,還是老老實(shí)實(shí)用最基本、最簡(jiǎn)單的隊(duì)列緩沖區(qū)吧。還有一點(diǎn)需要說(shuō)明一下:本文所提及的“存儲(chǔ)空間”,不僅包括內(nèi)存,還可能包括諸如硬盤之類的存儲(chǔ)介質(zhì)。
★環(huán)形緩沖區(qū) vs 隊(duì)列緩沖區(qū)
◇外部接口相似
在介紹環(huán)形緩沖區(qū)之前,咱們先來(lái)回顧一下普通的隊(duì)列。普通的隊(duì)列有一個(gè)寫入端和一個(gè)讀出端。隊(duì)列為空的時(shí)候,讀出端無(wú)法讀取數(shù)據(jù);當(dāng)隊(duì)列滿(達(dá)到最大尺寸)時(shí),寫入端無(wú)法寫入數(shù)據(jù)。
對(duì)于使用者來(lái)講,環(huán)形緩沖區(qū)和隊(duì)列緩沖區(qū)是一樣的。它也有一個(gè)寫入端(用于push)和一個(gè)讀出端(用于pop),也有緩沖區(qū)“滿”和“空”的狀態(tài)。所以,從隊(duì)列緩沖區(qū)切換到環(huán)形緩沖區(qū),對(duì)于使用者來(lái)說(shuō)能比較平滑地過(guò)渡。
◇內(nèi)部結(jié)構(gòu)迥異
雖然兩者的對(duì)外接口差不多,但是內(nèi)部結(jié)構(gòu)和運(yùn)作機(jī)制有很大差別。隊(duì)列的內(nèi)部結(jié)構(gòu)此處就不多啰嗦了。重點(diǎn)介紹一下環(huán)形緩沖區(qū)的內(nèi)部結(jié)構(gòu)。
大伙兒可以把環(huán)形緩沖區(qū)的讀出端(以下簡(jiǎn)稱R)和寫入端(以下簡(jiǎn)稱W)想象成是兩個(gè)人在體育場(chǎng)跑道上追逐(R追W)。當(dāng)R追上W的時(shí)候,就是緩沖區(qū)為空;當(dāng)W追上R的時(shí)候(W比R多跑一圈),就是緩沖區(qū)滿。
為了形象起見(jiàn),去找來(lái)一張圖并略作修改,如下:

從上圖可以看出,環(huán)形緩沖區(qū)所有的push和pop操作都是在一個(gè)固定的存儲(chǔ)空間內(nèi)進(jìn)行。而隊(duì)列緩沖區(qū)在push的時(shí)候,可能會(huì)分配存儲(chǔ)空間用于存儲(chǔ)新元素;在pop時(shí),可能會(huì)釋放廢棄元素的存儲(chǔ)空間。所以環(huán)形方式相比隊(duì)列方式,少掉了對(duì)于緩沖區(qū)元素所用存儲(chǔ)空間的分配、釋放。這是環(huán)形緩沖區(qū)的一個(gè)主要優(yōu)勢(shì)。
★環(huán)形緩沖區(qū)的實(shí)現(xiàn)
如果你手頭已經(jīng)有現(xiàn)成的環(huán)形緩沖區(qū)可供使用,并且你對(duì)環(huán)形緩沖區(qū)的內(nèi)部實(shí)現(xiàn)不感興趣,可以跳過(guò)這段。
◇數(shù)組方式 vs 鏈表方式
環(huán)形緩沖區(qū)的內(nèi)部實(shí)現(xiàn),即可基于數(shù)組(此處的數(shù)組,泛指連續(xù)存儲(chǔ)空間)實(shí)現(xiàn),也可基于鏈表實(shí)現(xiàn)。
數(shù)組在物理存儲(chǔ)上是一維的連續(xù)線性結(jié)構(gòu),可以在初始化時(shí),把存儲(chǔ)空間一次性分配好,這是數(shù)組方式的優(yōu)點(diǎn)。但是要使用數(shù)組來(lái)模擬環(huán),你必須在邏輯上把數(shù)組的頭和尾相連。在順序遍歷數(shù)組時(shí),對(duì)尾部元素(最后一個(gè)元素)要作一下特殊處理。訪問(wèn)尾部元素的下一個(gè)元素時(shí),要重新回到頭部元素(第0個(gè)元素)。如下圖所示:

使用鏈表的方式,正好和數(shù)組相反:鏈表省去了頭尾相連的特殊處理。但是鏈表在初始化的時(shí)候比較繁瑣,而且在有些場(chǎng)合(比如后面提到的跨進(jìn)程的IPC)不太方便使用。
◇讀寫操作
環(huán)形緩沖區(qū)要維護(hù)兩個(gè)索引,分別對(duì)應(yīng)寫入端(W)和讀取端(R)。寫入(push)的時(shí)候,先確保環(huán)沒(méi)滿,然后把數(shù)據(jù)復(fù)制到W所對(duì)應(yīng)的元素,最后W指向下一個(gè)元素;讀?。╬op)的時(shí)候,先確保環(huán)沒(méi)空,然后返回R對(duì)應(yīng)的元素,最后R指向下一個(gè)元素。
◇判斷“空”和“滿”
上述的操作并不復(fù)雜,不過(guò)有一個(gè)小小的麻煩:空環(huán)和滿環(huán)的時(shí)候,R和W都指向同一個(gè)位置!這樣就無(wú)法判斷到底是“空”還是“滿”。大體上有兩種方法可以解決該問(wèn)題。
辦法1:始終保持一個(gè)元素不用
當(dāng)空環(huán)的時(shí)候,R和W重疊。當(dāng)W比R跑得快,追到距離R還有一個(gè)元素間隔的時(shí)候,就認(rèn)為環(huán)已經(jīng)滿。當(dāng)環(huán)內(nèi)元素占用的存儲(chǔ)空間較大的時(shí)候,這種辦法顯得很土(浪費(fèi)空間)。
辦法2:維護(hù)額外變量
如果不喜歡上述辦法,還可以采用額外的變量來(lái)解決。比如可以用一個(gè)整數(shù)記錄當(dāng)前環(huán)中已經(jīng)保存的元素個(gè)數(shù)(該整數(shù)>=0)。當(dāng)R和W重疊的時(shí)候,通過(guò)該變量就可以知道是“空”還是“滿”。
◇元素的存儲(chǔ)
由于環(huán)形緩沖區(qū)本身就是要降低存儲(chǔ)空間分配的開(kāi)銷,因此緩沖區(qū)中元素的類型要選好。盡量存儲(chǔ)值類型的數(shù)據(jù),而不要存儲(chǔ)指針(引用)類型的數(shù)據(jù)。因?yàn)橹羔橆愋偷臄?shù)據(jù)又會(huì)引起存儲(chǔ)空間(比如堆內(nèi)存)的分配和釋放,使得環(huán)形緩沖區(qū)的效果打折扣。
★應(yīng)用場(chǎng)合
剛才介紹了環(huán)形緩沖區(qū)內(nèi)部的實(shí)現(xiàn)機(jī)制。按照前一個(gè)帖子的慣例,我們來(lái)介紹一下在線程和進(jìn)程方式下的使用。
如果你所使用的編程語(yǔ)言和開(kāi)發(fā)庫(kù)中帶有現(xiàn)成的、成熟的環(huán)形緩沖區(qū),強(qiáng)烈建議使用現(xiàn)成的庫(kù),不要重新制造輪子;確實(shí)找不到現(xiàn)成的,才考慮自己實(shí)現(xiàn)。如果你純粹是業(yè)余時(shí)間練練手,那另當(dāng)別論。
◇用于并發(fā)線程
和線程中的隊(duì)列緩沖區(qū)類似,線程中的環(huán)形緩沖區(qū)也要考慮線程安全的問(wèn)題。除非你使用的環(huán)形緩沖區(qū)的庫(kù)已經(jīng)幫你實(shí)現(xiàn)了線程安全,否則你還是得自己動(dòng)手搞定。線程方式下的環(huán)形緩沖區(qū)用得比較多,相關(guān)的網(wǎng)上資料也多,下面就大致介紹幾個(gè)。
對(duì)于C++的程序員,強(qiáng)烈推薦使用boost提供的circular_buffer模板,該模板最開(kāi)始是在boost 1.35版本中引入的。鑒于boost在C++社區(qū)中的地位,大伙兒應(yīng)該可以放心使用該模板。
對(duì)于C程序員,可以去看看開(kāi)源項(xiàng)目circbuf,不過(guò)該項(xiàng)目是GPL協(xié)議的,不太爽;而且活躍度不太高;而且只有一個(gè)開(kāi)發(fā)人員。大伙兒慎用!建議只拿它當(dāng)參考。
對(duì)于C#程序員,可以參考CodeProject上的一個(gè)示例。
◇用于并發(fā)進(jìn)程
進(jìn)程間的環(huán)形緩沖區(qū),似乎少有現(xiàn)成的庫(kù)可用。大伙兒只好自己動(dòng)手、豐衣足食了。
適用于進(jìn)程間環(huán)形緩沖的IPC類型,常見(jiàn)的有共享內(nèi)存和文件。在這兩種方式上進(jìn)行環(huán)形緩沖,通常都采用數(shù)組的方式實(shí)現(xiàn)。程序事先分配好一個(gè)固定長(zhǎng)度的存儲(chǔ)空間,然后具體的讀寫操作、判斷“空”和“滿”、元素存儲(chǔ)等細(xì)節(jié)就可參照前面所說(shuō)的來(lái)進(jìn)行。
共享內(nèi)存方式的性能很好,適用于數(shù)據(jù)流量很大的場(chǎng)景。但是有些語(yǔ)言(比如Java)對(duì)于共享內(nèi)存不支持。因此,該方式在多語(yǔ)言協(xié)同開(kāi)發(fā)的系統(tǒng)中,會(huì)有一定的局限性。
而文件方式在編程語(yǔ)言方面支持很好,幾乎所有編程語(yǔ)言都支持操作文件。但它可能會(huì)受限于磁盤讀寫(Disk I/O)的性能。所以文件方式不太適合于快速數(shù)據(jù)傳輸;但是對(duì)于某些“數(shù)據(jù)單元”很大的場(chǎng)合,文件方式是值得考慮的。
對(duì)于進(jìn)程間的環(huán)形緩沖區(qū),同樣要考慮好進(jìn)程間的同步、互斥等問(wèn)題,限于篇幅,此處就不細(xì)說(shuō)了。
下一個(gè)帖子,咱們來(lái)聊一下雙緩沖區(qū)的使用。
轉(zhuǎn)載必須包含本聲明、保持本文完整。并以超鏈接形式注明作者編程隨想和本文原始地址: http://program-think.blogspot.com/2009/03/producer-consumer-pattern-0-overview.html
|