架構設計:生產者/消費者模式[4]:雙緩衝區

無痕幽雨發表於2018-02-09

架構設計:生產者/消費者模式[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 型別中,比較適合用雙緩衝的主要是:共享記憶體和檔案。非常湊巧,這兩個玩意兒的特點和適用範圍在環形緩衝區的帖子裡面也已經介紹過了,俺又可以節省不少口水 :)

回到本系列的目錄

 

相關文章