C++性能之驚羣問題

何謂驚羣問題?

在這裏插入圖片描述

場景1:6只小鳥停在電線上休息,都在等待食物。
在這裏插入圖片描述
場景2:我們向鳥羣投放一條小蟲,作爲它們的食物。
在這裏插入圖片描述
場景3:6只小鳥看到有食物到來,都停止休息,一起飛起來去搶奪食物。
在這裏插入圖片描述
場景4:最終只有一隻小鳥(bird4)能夠喫到食物,其他小鳥無奈而又傷心的回到電線上繼續休息。
在這裏插入圖片描述
上面我們的小場景實際就是一個現實中的驚羣問題,明明只有一條小蟲子子到來,6只小鳥卻都要停止休息去搶奪食物,除了搶到食物的小鳥,其他搶不到食物的小鳥又需要重新飛回去休息,對於這部分小鳥來說,無謂浪費了很多體力。







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

驚羣問題出現場景

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

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

epoll函數中的驚羣問題

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

線程池中的驚羣問題

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

驚羣問題解決辦法

對於epll函數調用的驚羣問題解決辦法可以參考Nginx的解決辦法,多個進程將listenfd加入到epoll之前,首先嚐試獲取一個全局的accept_mutex互斥鎖,只有獲得該鎖的進程纔可以把listenfd加入到epoll中,當網絡連接事件到來時,只有epoll中含有listenfd的線程纔會被喚醒並處理網絡連接事件。從而解決了epoll調用中的驚羣問題。

對於線程池中的驚羣問題,我們需要分情況看待,有時候業務需求就是需要喚醒所有線程,那麼這時候使用notify_all()喚醒所有線程就不能稱爲”驚羣問題“,因爲CPU並沒有無謂消耗。而對於只需要喚醒一個線程的情況,我們需要使用notify_one()函數代替notify_all()只喚醒一個線程,從而避免驚羣問題。

在這裏插入圖片描述
在這裏插入圖片描述

最後小編推薦自己的Linux、C/C++技術交流羣:【960994558】整理了一些個人覺得比較好的學習書籍、視頻資料共享在裏面(包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK等等.),有需要的可以自行添加哦!~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章