多核並行編程的背景
在摩爾定律失效之前,提升處理器性能通過主頻提升、硬件超線程等技術就能滿足應用需要。隨着主頻提升慢慢接近撞上光速這道牆,摩爾定律開始逐漸失效,多核集成為處理器性能提升的主流手段。現在市面上已經很難看到單核的處理器,就是這一發展趨勢的佐證。要充分發揮多核豐富的計算資源優勢,多核下的並行編程就不可避免,Linux kernel就是一典型的多核並行編程場景。但多核下的並行編程卻挑戰多多。
多核並行編程的挑戰
目前主流的計算機都是馮諾依曼架構,即共享內存的計算模型,這種過程計算模型對並行計算並不友好。下圖是一種典型的計算機硬件體系架構。
這種架構中,有如下設計特點:
- 多個CPU核改善處理器的計算處理能力;
- 多級cache改善CPU訪問主存的效率;
- 各個CPU都有本地內存(NUMA(非一致性內存訪問)),進一步改善CPU訪問主存的效率;
- store buffer模塊改善cache write由於應答延遲而造成的寫停頓問題;
- invalidate queue模塊改善使無效應答的時延,把使無效命令放入queue后就立即發送應答;
- 外設DMA支持直接訪問主存,改善CPU使用效率;
這些硬件體系設計特點也引入很多問題,最大的問題就是cache一致性問題和亂序執行問題。
cache一致性問題由cache一致性協議MESI解決,MESI由硬件保證,對軟件來說是透明的。MESI協議保證所有CPU對單個cache line中單個變量修改的順序保持一致,但不保證不同變量的修改在所有CPU上看到的是相同順序。這就造成了亂序。不僅如此,亂序的原因還有很多:
- store buffer引起的延遲處理,會造成亂序;
- invalidate queue引起的延遲處理,會造成亂序;
- 編譯優化,會造成亂序;
- 分支預測、多流水線等CPU硬件優化技術,會造成亂序;
- 外設DMA,會造成數據亂序;
這種情況造成,就連簡單的++運算操作的原子性都無法保證。這些問題必須采用多核並行編程新的技術手段來解決。
多核並行編程關鍵技術
鎖技術
Linux kernel提供了多種鎖機制,如自旋鎖、信號量、互斥量、讀寫鎖、順序鎖等。各種鎖的簡單比較如下,具體實現和使用細節這里就不展開了,可以參考《Linux內核設計與實現》等書的相關章節。
- 自旋鎖,不休眠,無進程上下文切換開銷,可以用在中斷上下文和臨界區小的場合;
- 信號量,會休眠,支持同時多個並發體進入臨界區,可以用在可能休眠或者長的臨界區的場合;
- 互斥量,類似與信號量,但只支持同時只有一個並發體進入臨界區;
- 讀寫鎖,支持讀並發,寫寫/讀寫間互斥,讀會延遲寫,對讀友好,適用讀側重場合;
- 順序鎖,支持讀並發,寫寫/讀寫間互斥,寫會延遲讀,對寫友好,適用寫側重場合;
鎖技術雖然能有效地提供並行執行下的競態保護,但鎖的並行可擴展性很差,無法充分發揮多核的性能優勢。鎖的粒度太粗會限制擴展性,粒度太細會導致巨大的系統開銷,而且設計難度大,容易造成死鎖。除了並發可擴展性差和死鎖外,鎖還會引入很多其他問題,如鎖驚群、活鎖、飢餓、不公平鎖、優先級反轉等。不過也有一些技術手段或指導原則能解決或減輕這些問題的風險。
- 按統一的順序使用鎖(鎖的層次),解決死鎖問題;
- 指數后退,解決活鎖/飢餓問題;
- 范圍鎖(樹狀鎖),解決鎖驚群問題;
- 優先級繼承,解決優先級反轉問題 ;
原子技術
原子技術主要是解決cache和內存不一致性和亂序執行對原子訪問的破壞問題。Linux kernel中主要的原子原語有:
- ACCESS_ONCE()、READ_ONCE() and WRITE_ONCE():禁止編譯器對數據訪問的優化,強制從內存而不是緩存中獲取數據;
- barrier():亂序訪問內存屏障,限制編譯器的亂序優化;
- smb_wmb():寫內存屏障,刷新store buffer,同時限制編譯器和CPU的亂序優化;
- smb_rmb():讀內存屏障,刷新invalidate queue,同時限制編譯器和CPU的亂序優化;
- smb_mb():讀寫內存屏障,同時刷新store buffer和invalidate queue,同時限制編譯器和CPU的亂序優化;
- atomic_inc()/atomic_read()等:整型原子操作;
嚴格來說,Linux kernel作為系統軟件,實現受硬件影響很大,不同硬件有不同的內存模型,因此,不同於高級語言,Linux kernel的原子原語語義並沒有一個統一模型。比如在SMP的ARM64 CPU上,barrier、smb_wmb、smb_rmb的實現與smb_mb都是一樣的,都是volatile ("" ::: "memory")。
另外,再多提一句的是,atomic_inc()原語為了保證原子性,需要對cache進行刷新,而緩存行在多核體系下傳播相當耗時,其多核下的並行可擴展性差。
無鎖技術
上一小節中所提到的原子技術,是無鎖技術中的一種,除此之外,無鎖技術還包括RCU、Hazard pointer等。值得一提的是,這些無鎖技術都基於內存屏障實現的。
- Hazard pointer主要用於對象的生命周期管理,類似引用計數,但比引用計數有更好的並行可擴展性;
- RCU適用的場景很多,其可以替代:讀寫鎖、引用計數、垃圾回收器、等待事物結束等,而且有更好的並行擴展性。但RCU也有一些不適用的場景,如寫側重;臨界區長;臨界區內休眠等場景。
不過,所有的無鎖原語也只能解決讀端的並行可擴展性問題,寫端的並行可擴展性只能通過數據分割技術來解決。
數據分割技術
分割數據結構,減少共享數據,是解決並行可擴展性的根本辦法。對分割友好(即並行友好)的數據結構有:
- 數組
- 哈希表
- 基樹(Radix Tree)/稀疏數組
- 跳躍列表(skip list)
使用這些便於分割的數據結構,有利於我們通過數據分割來改善並行可擴展性。
除了使用合適的數據結構外,合理的分割指導規則也很重要:
- 讀寫分割:以讀為主的數據與以寫為主的數據分開;
- 路徑分割:按獨立的代碼執行路徑來分割數據;
- 專項分割:把經常更新的數據綁定到指定的CPU/線程中;
- 所有權分割:按CPU/線程個數對數據結構進行分割,把數據分割到per-cpu/per-thread中;
4種分割規則中,所有權分割是分割最徹底的。
以上這些多核並行編程內容基本上涵蓋了Linux kernel中所有的並發編程關鍵技術。當然並行編程還有很多其他技術沒有應用到Linux kernel中的,如無副作用的並行函數式編程技術(Erlang/Go等)、消息傳遞、MapReduce等等。