1. 程式人生 > >轉:架構設計:生產者/消費者模式[3]:環形緩衝區

轉:架構設計:生產者/消費者模式[3]:環形緩衝區

原文:http://blog.csdn.net/program_think/article/details/4040068

前一個帖子提及了佇列緩衝區可能存在的效能問題及解決方法:環形緩衝區。今天就專門來描述一下這個話題。
  為了防止有人給咱扣上“過度設計”的大帽子,事先宣告一下:只有當儲存空間的分配/釋放非常頻繁並且確實產生了明顯的影響,你才應該考慮環形緩衝區的使用。否則的話,還是老老實實用最基本、最簡單的佇列緩衝區吧。還有一點需要說明一下:本文所提及的“儲存空間”,不僅包括記憶體,還可能包括諸如硬碟之類的儲存介質。

  ★環形緩衝區 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)的效能。所以檔案方式不太適合於快速資料傳輸;但是對於某些“資料單元”很大的場合,檔案方式是值得考慮的。
  對於程序間的環形緩衝區,同樣要考慮好程序間的同步、互斥等問題,限於篇幅,此處就不細說了。
  下一個帖子,咱們來聊一下雙緩衝區的使用