生產(chǎn)者/消費(fèi)者模式

簡(jiǎn)介

在實(shí)際的軟件開發(fā)過程中,經(jīng)常會(huì)碰到如下場(chǎng)景:

  • 某個(gè)模塊負(fù)責(zé)產(chǎn)生數(shù)據(jù),這些數(shù)據(jù)由另一個(gè)模塊來負(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è)寄信的例子:

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)
    可能會(huì)有這樣的疑問:這個(gè)緩沖區(qū)有什么用?為什么不讓生產(chǎn)者直接調(diào)用消費(fèi)者的某個(gè)函數(shù),直接把數(shù)據(jù)傳遞過去?搞出這么一個(gè)緩沖區(qū)干什么?
    其實(shí)這里面是大有講究的,大概有如下一些好處。

解耦

  • 假設(shè)生產(chǎn)者和消費(fèi)者分別是兩個(gè)類。
  • 如果讓生產(chǎn)者直接調(diào)用消費(fèi)者的某個(gè)方法,那么生產(chǎn)者對(duì)于消費(fèi)者就會(huì)產(chǎn)生依賴(也就是耦合)。
  • 將來如果消費(fèi)者的代碼發(fā)生變化,可能會(huì)影響到生產(chǎn)者。
  • 而如果兩者都依賴于某個(gè)緩沖區(qū),兩者之間不直接依賴,耦合也就相應(yīng)降低了。
    接著上述的例子,如果不使用郵筒(也就是緩沖區(qū)),你必須得把信直接交給郵遞員。有同學(xué)會(huì)說,直接給郵遞員不是挺簡(jiǎn)單的嘛?其實(shí)不簡(jiǎn)單,你必須得認(rèn)識(shí)誰是郵遞員,才能把信給他(光憑身上穿的制服,萬一有人假冒,就慘了)。這就產(chǎn)生和你和郵遞員之間的依賴(相當(dāng)于生產(chǎn)者和消費(fèi)者的強(qiáng)耦合)。萬一哪天郵遞員換人了,你還要重新認(rèn)識(shí)一下(相當(dāng)于消費(fèi)者變化導(dǎo)致修改生產(chǎn)者代碼)。而郵筒相對(duì)來說比較固定,你依賴它的成本就比較低(相當(dāng)于和緩沖區(qū)之間的弱耦合)。

支持并發(fā)(concurrency)

  • 生產(chǎn)者直接調(diào)用消費(fèi)者的某個(gè)方法,還有另一個(gè)弊端。

  • 由于函數(shù)調(diào)用是同步的(或者叫阻塞的),在消費(fèi)者的方法沒有返回之前,生產(chǎn)者只好一直等在那邊。萬一消費(fèi)者處理數(shù)據(jù)很慢,生產(chǎn)者就會(huì)白白糟蹋大好時(shí)光。

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


    架構(gòu)設(shè)計(jì):生產(chǎn)者/消費(fèi)者模式

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


    架構(gòu)設(shè)計(jì):生產(chǎn)者/消費(fè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)不感興趣,可以跳過這段。
    ◇數(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ù)組來模擬環(huán),你必須在邏輯上把數(shù)組的頭和尾相連。在順序遍歷數(shù)組時(shí),對(duì)尾部元素(最后一個(gè)元素)要作一下特殊處理。訪問尾部元素的下一個(gè)元素時(shí),要重新回到頭部元素(第0個(gè)元素)。如下圖所示:


    架構(gòu)設(shè)計(jì):生產(chǎn)者/消費(fèi)者模式

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容