亚洲免费在线-亚洲免费在线播放-亚洲免费在线观看-亚洲免费在线观看视频-亚洲免费在线看-亚洲免费在线视频

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

系統 1682 0

2009-03 作者: 編程隨想 來源: 編程隨想的博客

[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] :如何確定數據單元?

既然 前一個帖子 已經搞過掃盲了,那接下來應該開始聊一些具體的編程技術問題了。不過在進入具體的技術細節之前,咱們先要搞明白一個問題:如何確定數據單元?只有把數據單元分析清楚,后面的技術設計才好搞。

★啥是數據單元

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

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

★數據單元的特性

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

◇關聯到業務對象

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

由于“寄信 這個業務邏輯比較簡單,所以大伙兒很容易就可以判斷出數據單元是啥。但現實生活中,往往沒這么樂觀。大多數業務邏輯都比較復雜,當中包含的業務對象是層次繁多、類型各異。在這種情況下,就不易作出決策了。

這一步很重要,如果選錯了業務對象,會導致后續程序設計和編碼實現的復雜度大為上升,增加了開發和維護成本。

(例如,在進行 MPEG-4 碼流緩沖時,可考慮以一幀作為數據單元,也可以以一個 GOP 作為數據單元,視具體業務而定)

◇完整性

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

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

(例如,在進行 MPEG-4 碼流緩沖時,數據必須一整幀或一個完整的 GOP

◇獨立性

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

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

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

(例如,在進行 MPEG-4 碼流緩沖時,幀與幀之間一般有差向關聯,而一個 GOP 是獨立的)

◇顆粒度

前面提到,數據單元需要關聯到某種業務對象。那么數據單元和業務對象是否要一一對應捏?很多場合確實是一一對應的。

不過,有時出于性能等因素的考慮,也可能會 N 個業務對象打包成一個數據單元 (比如 MPEG-4 碼流中多幀組成 GOP )。那么,這個 N 該如何取值就是顆粒度的考慮了。顆粒度的大小是有講究的。太大的顆粒度可能會造成某種浪費;太小的顆粒度可能會造成性能問題。顆粒度的權衡要基于多方面的因素,以及一些經驗值的考量。

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

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

可能有同學會問:生產者和消費者的顆粒度能否設置成不同大小(比如對于寄信人設置成 1 ,對于郵遞員設置成 100 )。當然,理論上可以這么干,但是在某些情況下會增加程序邏輯和代碼實現的復雜度。后面討論具體技術細節時,或許會聊到這個問題。

(例如,在進行 MPEG-4 碼流緩沖時,視情況一次可緩沖多個 GOP

好,數據單元的話題就說到這。希望通過本帖子,大伙兒能夠搞明白數據單元到底是怎么一回事。下一個帖子,咱們來聊一下“ 基于隊列的緩沖區 ,技術上如何實現。

[2] :隊列緩沖區

經過前面兩個帖子的鋪墊,今天終于開始聊一些具體的編程技術了。由于不同的緩沖區類型、不同的并發場景對于具體的技術實現有較大的影響。為了深入淺出、便于大伙兒理解,咱們先來介紹最傳統、最常見的方式。也就是單個生產者對應單個消費者,當中用隊列( FIFO )作緩沖。

關于并發的場景,在之前的帖子“ 進程還線程?是一個問題! 中,已經專門論述了進程和線程各自的優缺點,兩者皆不可偏廢。所以,后面對各種緩沖區類型的介紹都會同時提及進程方式和線程方式。

★線程方式

先來說一下并發線程中使用隊列的例子,以及相關的優缺點。

◇內存分配的性能

在線程方式下,生產者和消費者各自是一個線程。生產者把數據寫入隊列頭(以下簡稱 push ),消費者從隊列尾部讀出數據(以下簡稱 pop )。當隊列為空,消費者就稍息(稍事休息);當隊列滿(達到最大長度),生產者就稍息。整個流程并不復雜。

那么,上述過程會有什么問題捏?一個主要的問題是關于內存分配的性能開銷。對于常見的隊列實現:在每次 push 時,可能涉及到堆內存的分配;在每次 pop 時,可能涉及堆內存的釋放。假如生產者和消費者都很勤快,頻繁地 push pop ,那內存分配的開銷就很可觀了。對于內存分配的開銷,用 Java 的同學可以參見前幾天的帖子“ Java 性能優化[1] ;對于用 C/C++ 的同學,想必對 OS 底層機制會更清楚,應該知道分配堆內存( new malloc )會有加鎖的開銷和 用戶態/核心態切換 的開銷。

那該怎么辦捏?請聽下文分解,關于“ 生產者/消費者模式[3] :環形緩沖區

◇同步和互斥的性能

另外,由于兩個線程共用一個隊列,自然就會涉及到線程間諸如同步啊、互斥啊、死鎖啊等等勞心費神的事情。好在“操作系統 這門課程對此有詳細介紹,學過的同學應該還有點印象吧?對于沒學過這門課的同學,也不必難過,網上相關的介紹挺多的(比如“ 這里 ),大伙自己去瞅一瞅。關于這方面的細節,咱今天就不多啰嗦了。

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

這又該咋辦捏?請聽下文的分解,關于“ 生產者/消費者模式[4] :雙緩沖區

◇適用于隊列的場合

剛才盡批判了隊列的缺點,難道隊列方式就一無是處?非也。由于隊列是很常見的數據結構,大部分編程語言都內置了隊列的支持(具體介紹見“ 這里 ),有些語言甚至提供了線程安全的隊列(比如 JDK 1.5 引入的 ArrayBlockingQueue )。因此,開發人員可以撿現成,避免了重新發明輪子。

所以, 假如你的數據流量不是很大,采用隊列緩沖區的好處還是很明顯的 :邏輯清晰、代碼簡單、維護方便。比較符合 KISS 原則。

★進程方式

說完了線程的方式,再來介紹基于進程的并發。

跨進程的生產者/消費者模式,非常依賴于具體的進程間通訊( IPC )方式。而 IPC 的種類名目繁多,不便于挨個列舉(畢竟口水有限)。因此咱們挑選幾種跨平臺、且編程語言支持較多的 IPC 方式來說事兒。

◇匿名管道

感覺管道是最像隊列的 IPC 類型。生產者進程在管道的 寫端 放入數據;消費者進程在管道的 讀端 取出數據。整個的效果和線程中使用隊列非常類似,區別在于使用管道就無需操心線程安全、內存分配等瑣事(操作系統暗中都幫你搞定了)。

管道又分 命名管道 匿名管道 兩種,今天主要聊匿名管道。因為命名管道在不同的操作系統下差異較大(比如 Win32 POSIX ,在命名管道的 API 接口和功能實現上都有較大差異;有些平臺不支持命名管道,比如 Windows CE 除了操作系統的問題,對于有些編程語言(比如 Java )來說,命名管道是無法使用的。所以我一般不推薦使用這玩意兒。

其實匿名管道在不同平臺上的 API 接口,也是有差異的(比如 Win32 CreatePipe POSIX pipe ,用法就很不一樣)。但是我們可以僅使用標準輸入和標準輸出(以下簡稱 stdio )來進行數據的流入流出。然后利用 shell 的管道符把生產者進程和消費者進程關聯起來(沒聽說過這種手法的同學,可以看“ 這里 )。實際上,很多操作系統(尤其是 POSIX 風格的)自帶的命令都充分利用了這個特性來實現數據的傳輸(比如 more grep 等)。

這么干有幾個好處:

1 、基本上所有操作系統都支持在 shell 方式下使用管道符。因此很容易實現跨平臺。

2 、大部分編程語言都能夠操作 stdio ,因此跨編程語言也就容易實現。

3 、剛才已經提到,管道方式省卻了線程安全方面的瑣事。有利于降低開發、調試成本。

當然,這種方式也有自身的缺點:

1 、生產者進程和消費者進程必須得在同一臺主機上,無法跨機器通訊。這個缺點比較明顯。

2 、在一對一的情況下,這種方式挺合用。但如果要擴展到一對多或者多對一,那就有點棘手了。所以這種方式的擴展性要打個折扣。假如今后要考慮類似的擴展,這個缺點就比較明顯。

3 、由于管道是 shell 創建的,對于兩邊的進程不可見(程序看到的只是 stdio )。在某些情況下,導致程序不便于對管道進行操縱(比如調整管道緩沖區尺寸)。這個缺點不太明顯。

4 、最后,這種方式只能單向傳數據。好在大多數情況下,消費者進程不需要傳數據給生產者進程。萬一你確實需要信息反饋(從消費者到生產者),那就費勁了。可能得考慮換種 IPC 方式。

順便補充幾個注意事項,大伙兒留意一下:

1 、對 stdio 進行讀寫操作是以阻塞方式進行。比如管道中沒有數據,消費者進程的讀操作就會一直停在哪兒,直到管道中重新有數據。

2 、由于 stdio 內部帶有自己的緩沖區(這緩沖區和管道緩沖區是兩碼事),有時會導致一些不太爽的現象(比如生產者進程輸出了數據,但消費者進程沒有立即讀到)。具體的細節,大伙兒可以看“ 這里

SOCKET TCP 方式)

基于 TCP 方式的 SOCKET 通訊是又一個類似于隊列的 IPC 方式。它同樣保證了數據的順序到達;同樣有緩沖的機制。而且這玩意兒也是跨平臺和跨語言的,和剛才介紹的 shell 管道符方式類似。

SOCKET 相比 shell 管道符的方式,有啥優點捏?主要有如下幾個優點:

1 SOCKET 方式可以跨機器(便于實現分布式)。這是主要優點。

2 SOCKET 方式便于將來擴展成為多對一或者一對多。這也是主要優點。

3 SOCKET 可以設置阻塞和非阻塞方法,用起來比較靈活。這是次要優點。

4 SOCKET 支持雙向通訊,有利于消費者反饋信息。

當然有利就有弊。相對于上述 shell 管道的方式,使用 SOCKET 在編程上會更復雜一些。好在前人已經做了大量的工作,搞出很多 SOCKET 通訊庫和框架給大伙兒用(比如 C++ ACE 庫、 Python Twisted )。借助于這些第三方的庫和框架, SOCKET 方式用起來還是比較爽的。由于具體的網絡通訊庫該怎么用不是本系列的重點,此處就不細說了。

雖然 TCP 在很多方面比 UDP 可靠,但鑒于跨機器通訊先天的不可預料性(比如網線可能被某傻 X 給拔錯了,網絡的忙閑波動可能很大),在程序設計上我們還是要多留一手。具體該如何做捏?可以在生產者 進程 和消費者 進程 內部各自再引入基于 線程 的“生產者/消費者模式 。這話聽著像繞口令,為了便于理解,畫張圖給大伙兒瞅一瞅。

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

這么做的關鍵點在于把代碼分為兩部分:生產線程和消費線程屬于和業務邏輯相關的代碼(和通訊邏輯無關);發送線程和接收線程屬于通訊相關的代碼(和業務邏輯無關)。

這樣的好處是很明顯的,具體如下:

1 、能夠應對 暫時性 的網絡故障。并且在網絡故障解除后,能夠繼續工作。

2 、網絡故障的應對處理方式(比如斷開后的嘗試重連),只影響發送和接收線程,不會影響生產線程和消費線程(業務邏輯部分)。

3 、具體的 SOCKET 方式(阻塞和非阻塞)只影響發送和接收線程,不影響生產線程和消費線程(業務邏輯部分)。

4 、不依賴 TCP 自身的發送緩沖區和接收緩沖區。(默認的 TCP 緩沖區的大小可能無法滿足實際要求)

5 、業務邏輯的變化(比如業務需求變更)不影響發送線程和接收線程。

針對上述的最后一條,再多啰嗦幾句。如果整個業務系統中有多個進程是采用上述的模式,那或許可以重構一把:在業務邏輯代碼和通訊邏輯代碼之間切一刀,把業務邏輯無關的部分封裝成一個通訊中間件(說中間件顯得比較牛 X :- )。如果大伙兒對這玩意兒有興趣,以后專門開個帖子聊。

下一個帖子 ,咱們介紹一下環形緩沖區的話題。

[3] :環形緩 沖區

前一個帖子 提及了隊列緩沖區可能存在的性能問題及解決方法:環形緩沖區。今天就專門來描述一下這個話題。

為了防止有人給咱扣上“過度設計 的大帽子,事先聲明一下:只有當存儲空間的分配/釋放非常 頻繁 并且確實產生了 明顯 的影響,你才應該考慮環形緩沖區的使用。否則的話,還是老老實實用最基本、最簡單的 隊列緩沖區 吧。還有一點需要說明一下:本文所提及的“ 存儲空間 ,不僅包括內存,還可能包括諸如硬盤之類的存儲介質。

★環形緩沖區 vs 隊列緩沖區

◇外部接口相似

在介紹環形緩沖區之前,咱們先來回顧一下普通的隊列。普通的隊列有一個寫入端和一個讀出端。隊列為空的時候,讀出端無法讀取數據;當隊列滿(達到最大尺寸)時,寫入端無法寫入數據。

對于使用者來講,環形緩沖區和隊列緩沖區是一樣的。它也有一個寫入端(用于 push )和一個讀出端(用于 pop ),也有緩沖區“滿 和“空 的狀態。所以,從隊列緩沖區切換到環形緩沖區,對于使用者來說能比較平滑地過渡。

◇內部結構迥異

雖然兩者的對外接口差不多,但是內部結構和運作機制有很大差別。隊列的內部結構此處就不多啰嗦了。重點介紹一下環形緩沖區的內部結構。

大伙兒可以把環形緩沖區的讀出端(以下簡稱 R )和寫入端(以下簡稱 W )想象成是兩個人在體育場跑道上追逐( R W )。當 R 追上 W 的時候,就是緩沖區為空;當 W 追上 R 的時候( W R 多跑一圈),就是緩沖區滿。

為了形象起見,去找來一張圖并略作修改,如下:

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

從上圖可以看出,環形緩沖區所有的 push pop 操作都是在一個 固定 的存儲空間內進行。而隊列緩沖區在 push 的時候,可能會分配存儲空間用于存儲新元素;在 pop 時,可能會釋放廢棄元素的存儲空間。所以環形方式相比隊列方式,少掉了對于緩沖區元素所用存儲空間的分配、釋放。這是環形緩沖區的一個主要優勢。

★環形緩沖區的實現

如果你手頭已經有現成的環形緩沖區可供使用,并且你對環形緩沖區的內部實現不感興趣,可以跳過這段。

◇數組方式 vs 鏈表方式

環形緩沖區的內部實現,即可基于數組(此處的數組,泛指連續存儲空間)實現,也可基于鏈表實現。

數組在物理存儲上是一維的連續線性結構,可以在初始化時,把存儲空間 一次性 分配好,這是數組方式的優點。但是要使用數組來模擬環,你必須在邏輯上把數組的頭和尾相連。在順序遍歷數組時,對尾部元素(最后一個元素)要作一下特殊處理。訪問尾部元素的下一個元素時,要重新回到頭部元素(第 0 個元素)。如下圖所示:

使用鏈表的方式,正好和數組相反:鏈表省去了頭尾相連的特殊處理。但是鏈表在初始化的時候比較繁瑣,而且在有些場合(比如后面提到的跨進程的 IPC )不太方便使用。

◇讀寫操作

環形緩沖區要維護兩個索引,分別對應寫入端( W )和讀取端( R )。寫入( push )的時候,先確保環沒滿,然后把數據復制到 W 所對應的元素,最后 W 指向下一個元素;讀取( pop )的時候,先確保環沒空,然后返回 R 對應的元素,最后 R 指向下一個元素。

◇判斷 滿

上述的操作并不復雜,不過有一個小小的麻煩:空環和滿環的時候, R W 都指向同一個位置!這樣就無法判斷到底是 還是 滿 。大體上有兩種方法可以解決該問題。

辦法 1 :始終保持一個元素不用

當空環的時候, R W 重疊。當 W R 跑得快,追到距離 R 還有一個元素間隔的時候,就認為環已經滿。當環內元素占用的存儲空間較大的時候,這種辦法顯得很土(浪費空間)。

辦法 2 :維護額外變量

如果不喜歡上述辦法,還可以采用額外的變量來解決。比如可以用一個整數記錄當前環中已經保存的元素個數(該整數 >=0 )。當 R W 重疊的時候,通過該變量就可以知道是 還是 滿

◇元素的存儲

由于環形緩沖區本身就是要降低存儲空間分配的開銷,因此緩沖區中元素的類型要選好。盡量存儲 值類型 的數據,而不要存儲 指針(引用)類型 的數據。因為指針類型的數據又會引起存儲空間(比如堆內存)的分配和釋放,使得環形緩沖區的效果打折扣。

★應用場合

剛才介紹了環形緩沖區內部的實現機制。按照 前一個帖子 的慣例,我們來介紹一下在線程和進程方式下的使用。

如果你所使用的編程語言和開發庫中帶有現成的、 成熟的 環形緩沖區,強烈建議使用現成的庫,不要重新制造輪子;確實找不到現成的,才考慮自己實現。如果你純粹是業余時間練練手,那另當別論。

◇用于并發線程

和線程中的隊列緩沖區類似,線程中的環形緩沖區也要考慮線程安全的問題。除非你使用的環形緩沖區的庫已經幫你實現了線程安全,否則你還是得自己動手搞定。線程方式下的環形緩沖區用得比較多,相關的網上資料也多,下面就大致介紹幾個。

對于 C++ 的程序員,強烈推薦使用 boost 提供的 circular_buffer 模板,該模板最開始是在 boost 1.35 版本中引入的。鑒于 boost C++ 社區中的地位,大伙兒應該可以放心使用該模板。

對于 C 程序員,可以去看看開源項目 circbuf ,不過該項目是 GPL 協議的,不太爽;而且活躍度不太高;而且只有一個開發人員。大伙兒慎用!建議只拿它當參考。

對于 C# 程序員,可以參考 CodeProject 上的一個示例

◇用于并發進程

進程間的環形緩沖區,似乎少有現成的庫可用。大伙兒只好自己動手、豐衣足食了。

適用于進程間環形緩沖的 IPC 類型,常見的有 共享內存 和文件。在這兩種方式上進行環形緩沖,通常都采用數組的方式實現。程序事先分配好一個固定長度的存儲空間,然后具體的讀寫操作、判斷 滿 、元素存儲等細節就可參照前面所說的來進行。

共享內存方式的性能很好,適用于數據流量很大的場景。 但是有些語言(比如 Java )對于共享內存不支持。因此,該方式在多語言協同開發的系統中,會有一定的局限性。

而文件方式在編程語言方面支持很好,幾乎所有編程語言都支持操作文件。但它可能會受限于磁盤讀寫( Disk I/O )的性能。所以文件方式不太適合于快速數據傳輸;但是對于某些“ 數據單元 很大的場合,文件方式是值得考慮的。

對于進程間的環形緩沖區,同樣要考慮好進程間的同步、互斥等問題,限于篇幅,此處就不細說了。

下一個帖子 ,咱們來聊一下雙緩沖區的使用。

[ 4 ] 緩沖區

雙緩沖區 是一個應用很廣的手法。該手法用得最多的地方想必是屏幕繪制相關的領域(主要是為了減少屏幕閃爍)。另外,在設備驅動和工控方面,雙緩沖也經常被使用。不過今天要聊的,并不是針對上述的某個具體領域,而是側重于并發方面的同步 / 互斥開銷。另外提醒一下,雙緩沖方式和前面提到的隊列緩沖、環形緩沖是可以結合使用滴。

為啥要雙緩沖區

記得前幾天在 介紹隊列緩沖區 時,提及了普通隊列緩沖區的兩個性能問題: 內存分配的開銷 同步 / 互斥的開銷 (健忘的同學,先回去看看 那個帖子 復習一下)。 內存分配的開銷 已經在 介紹環形緩沖區 的時候解決了,而今天要介紹的雙緩沖區,就是沖著同步 / 互斥的開銷來的。

為了防止有人給咱扣上 過度設計 的大帽子,又得來一個事先聲明:只有當同步或互斥的開銷非常明顯的時候,你才應該考慮雙緩沖區的使用。否則的話,大伙兒還是老老實實用最基本、最簡單的隊列緩沖區吧。

雙緩沖區的原理

前面說了一通廢話,現在開始切入正題,說說具體實現。

所謂 雙緩沖區 ,故名思義就是要有倆緩沖區(簡稱 A B )。這倆緩沖區,總是一個用于生產者,另一個用于消費者。當倆緩沖區都操作完,再進行一次切換(先前被生產者寫入的轉為消費者讀出,先前消費者讀取的轉為生產者寫入)。由于生產者和消費者不會 同時 操作 同一個 緩沖區(不發生沖突),所以就不需要在讀寫 每一個 數據單元 的時候都進行同步 / 互斥操作。順便提一下,這又一次展現了 空間換時間 的優化思路。

但是光有倆緩沖區還不夠。為了真正做到 不沖突 ,還得再搞兩個互斥鎖(簡稱 La Lb ),分別對應倆緩沖區。生產者或消費者如果要操作某個緩沖區,必須先擁有對應的互斥鎖。補充一句:要達到 不沖突 的效果,其實可以有多種搞法,今天只是挑一個簡單的來聊。

雙緩沖區的幾種狀態

為了加深某些同學的理解,再描述一下雙緩沖區的幾種狀態。

◇倆緩沖區都在使用的狀態(并發讀寫)

大多數情況下,生產者和消費者都處于并發讀寫狀態。不妨設生產者寫入 A ,消費者讀取 B 。在這種狀態下,生產者擁有鎖 La ;同樣的,消費者擁有鎖 Lb 。由于倆緩沖區都是處于獨占狀態,因此每次讀寫緩沖區中的元素( 數據單元 )都 不需要 再進行加鎖、解鎖操作。這是節約開銷的主要來源。

◇單個緩沖區空閑的狀態

由于兩個并發實體的速度會有差異,必然會出現一個緩沖區已經操作完,而另一個尚未操作完。不妨假設生產者快于消費者。

在這種情況下,當生產者把 A 寫滿的時候,生產者要先釋放 La (表示它已經不再操作 A ),然后嘗試獲取 Lb 。由于 B 還沒有被讀空, Lb 還被消費者持有,所以生產者進入發呆( Suspend )狀態。

◇緩沖區的切換

接著上面的話題。

過了若干時間,消費者終于把 B 讀完。這時候,消費者也要先釋放 Lb ,然后嘗試獲取 La 。由于 La 剛才已經被生產者釋放,所以消費者能立即擁有 La 并開始讀取 A 的數據。而由于 Lb 被消費者釋放,所以剛才發呆的生產者會緩過神來( Resume )并擁有 Lb ,然后生產者繼續往 B 寫入數據。

經過上述幾個步驟,倆緩沖區完成了 對調 ,變為:生產者寫入 B ,消費者讀取 A

(對于雙緩沖,可引入序列號機制。)

可能的并發問題

本來單個緩沖區的生產者 / 消費者問題就已經是教科書的經典問題了,現在搞出倆緩沖區,所以就更加耗費腦細胞了。一不小心,就會搞出些并發的 Bug ,而且并發的 Bug 還很難調試和測試(這也就是為啥不要輕易使用該玩意兒的原因)。

◇死鎖的問題

假如把前面介紹的操作步驟調換一下順序:生產者或消費者在操作完當前的緩沖區之后,先去獲取另一個緩沖區的鎖,再來釋放當前緩沖區的鎖。那會咋樣捏?

一旦兩個并發實體 同時 處理完各自緩沖區,然后 同時 去獲取對方擁有的鎖,那就會出現典型的死鎖(死鎖的詳細解釋參見 這里 )場景。它倆從此陷入萬劫不復的境地。

應用場景

介紹完并發問題,按照 本系列 的慣例,最后再來介紹一下雙緩沖區在某些場合的應用。

◇用于并發線程

在線程方式下,首先要考慮的是緩沖區的類型:到底用隊列方式還是環形方式。這方面的選擇依據在 介紹環形緩沖區 的時候已經闡述過了,此處不再啰嗦(省去不少口水)。

另一個需要注意的是,某些編程語言或者程序庫提供了的線程安全的緩沖區(比如 JDK 1.5 引入的 ArrayBlockingQueue )。由于這種緩沖區會自動為每次的讀寫進行同步 / 互斥,所以就把雙緩沖的優勢抵消掉了。因此,大伙兒在進行緩沖區選型的時候要避開這類緩沖區。

◇用于并發進程

在進程間使用雙緩沖,先得考察不同 IPC 類型的特點。由于今天討論雙緩沖的目的是降低同步 / 互斥的開銷,對于那些已經封裝了同步 / 互斥的 IPC 類型,就沒太大必要再去搞雙緩沖了(單憑這條就足以讓好多種 IPC 出局)。剩下的 IPC 類型中,比較適合用雙緩沖的主要是:共享內存和文件。非常湊巧,這兩個玩意兒的特點和適用范圍在 環形緩沖區的帖子 里面也已經介紹過了,俺又可以節省不少口水 :)

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


更多文章、技術交流、商務合作、聯系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯系: 360901061

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。

【本文對您有幫助就好】

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長會非常 感謝您的哦!!!

發表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 天天摸日日碰天天看免费 | 日本综合欧美一区二区三区 | 国产精品在线观看 | 日本亚洲成高清一区二区三区 | 最新国产精品自拍 | 欧美日韩久久中文字幕 | 国产一级免费 | videos欧美丰满肥婆 | 欧美毛片在线播放观看 | 秋霞在线观看成人高清视频51 | 日韩精品视频美在线精品视频 | 亚洲欧美动漫 | 日本三级中文 | 亚洲精品久久精品h成人 | 成人短视频网站 | 日韩中字在线 | 欧美另类高清xxxxx | 日日摸天天摸狠狠摸视频 | 久久99久久99精品 | 亚洲国产天堂久久综合 | 插插天天| 久久这里只精品热在线8 | 国产在线精品观看一区 | 久久99热久久精品23 | 国产精品久久久久久久久久一区 | 国产午夜精品一二区理论影院 | 日韩中文字幕推理片 | 91久久夜色精品国产网站 | 国产精品高清一区二区 | 99久久精品国产交换 | 一级黄色录像免费观看 | 国产精品久久久久久影视 | 偷拍清纯高清视频在线 | 这里只有精品99re在线 | 桃花福利视频在线观看 | 中文字幕在线免费观看 | 亚洲国产成人在线 | 久久久久免费 | 丰满放荡岳乱妇91www | xx性欧美高清 | 俄罗斯一级在线播放 |