C++性能榨汁機之驚群問題

C++性能榨汁機之驚群問題

來自專欄 C++性能榨汁機

一個小場景

在開始正式討論我們的問題之前,我們先想像這麼一個小場景:

場景1:6隻小鳥停在電線上休息,都在等待食物。

場景2:我們向鳥群投放一條小蟲,作為它們的食物。

場景3:6隻小鳥看到有食物到來,都停止休息,一起飛起來去搶奪食物。

場景4:最終只有一隻小鳥(bird4)能夠吃到食物,其他小鳥無奈而又傷心的回到電線上繼續休息。

何謂驚群問題?

上面我們的小場景實際就是一個現實中的驚群問題,明明只有一條小蟲子子到來,6隻小鳥卻都要停止休息去搶奪食物,除了搶到食物的小鳥,其他搶不到食物的小鳥又需要重新飛回去休息,對於這部分小鳥來說,無謂浪費了很多體力。

那麼計算機中驚群又是什麼樣呢?其實與上述場景類似,多個線程(或者進程)同時等待一個事件的到來並準備處理事件,當事件到達時,把所有等待該事件的線程(或進程)均喚醒,但是只能有一個線程最終可以獲得事件的處理權,其他所有線程又重新陷入睡眠等待下次事件到來。這種線程被頻繁喚醒卻又沒有真正處理事件導致CPU無謂浪費稱為計算機中的「驚群問題」。

驚群問題出現場景

  1. Linux2.6內核版本之前系統API中的accept調用

    在Linux2.6內核版本之前,當多個線程中的accept函數同時監聽同一個listenfd的時候,如果此listenfd變成可讀,則系統會喚醒所有使用accept函數等待listenfd的所有線程(或進程),但是最終只有一個線程可以accept調用返回成功,其他線程的accept函數調用返回EAGAIN錯誤,線程回到等待狀態,這就是accept函數產生的驚群問題。但是在Linux2.6版本之後,內核解決了accept函數的驚群問題,當內核收到一個連接之後,只會喚醒等待隊列上的第一個線程(或進程),從而避免了驚群問題。

  2. epoll函數中的驚群問題

    如果我們使用多線程epoll對同一個fd進行監控的時候,當fd事件到來時,內核會把所有epoll線程喚醒,因此產生驚群問題。為何內核不能像解決accept問題那樣解決epoll的驚群問題呢?內核可以解決accept調用中的驚群問題,是因為內核清楚的知道accept調用只可能一個線程調用成功,其他線程必然失敗。而對於epoll調用而言,內核不清楚到底有幾個線程需要對該事件進行處理,所以只能將所有線程全部喚醒。
  3. 線程池中的驚群問題

    在實際應用程序開發中,為了避免線程的頻繁創建銷毀,我們一般建立線程池去並發處理,而線程池最經典的模型就是生產者-消費者模型,包含一個任務隊列,當隊列不為空的時候,線程池中的線程從任務隊列中取出任務進行處理。一般使用條件變數進行處理,當我們往任務隊列中放入任務時,需要喚醒等待的線程來處理任務,如果我們使用C++標準庫中的函數notify_all()來喚醒線程,則會將所有的線程都喚醒,然後最終只有一個線程可以獲得任務的處理權,其他線程在此陷入睡眠,因此產生驚群問題。

驚群問題解決辦法

  1. 對於epll函數調用的驚群問題解決辦法可以參考Nginx的解決辦法,多個進程將listenfd加入到epoll之前,首先嘗試獲取一個全局的accept_mutex互斥鎖,只有獲得該鎖的進程才可以把listenfd加入到epoll中,當網路連接事件到來時,只有epoll中含有listenfd的線程才會被喚醒並處理網路連接事件。從而解決了epoll調用中的驚群問題。
  2. 對於線程池中的驚群問題,我們需要分情況看待,有時候業務需求就是需要喚醒所有線程,那麼這時候使用notify_all()喚醒所有線程就不能稱為」驚群問題「,因為CPU並沒有無謂消耗。而對於只需要喚醒一個線程的情況,我們需要使用notify_one()函數代替notify_all()只喚醒一個線程,從而避免驚群問題。

推薦閱讀:

ServiceDesk Plus配置郵件自動轉換為服務類工單
python tips
電腦改變我們的生活
Luos UTT 學習筆記之五:強[可]正規性-下
關於使用電腦的九個誤區

TAG:計算機科學 | C | 編程 |