第六章主要介紹了 C++11 中的原子類型及其相關的API,原子類型的大多數 API 都須要程序員提供一個 std::memory_order(可譯爲內存序,訪存順序) 的枚舉類型值做爲參數,好比:atomic_store,atomic_load,atomic_exchange,atomic_compare_exchange 等 API 的最後一個形參爲 std::memory_order order,默認值是 std::memory_order_seq_cst(順序一致性)。那麼究竟什麼是 std::memory_order 呢,爲了解答這個問題,咱們先來討論 C++11 的內存模型。git
通常來說,內存模型可分爲靜態內存模型和動態內存模型,靜態內存模型主要涉及類的對象在內存中是如何存放的,即從結構(structural)方面來看一個對象在內存中的佈局,以一個簡單的例子爲例(截圖參考《C++ Concurrency In Action》 P105 ):程序員
上面是一個簡單的 C++ 類(又稱POD: Plain Old Data,它沒有虛函數,沒有繼承),它在內存中的佈局如圖右邊所示(對於複雜類對象的內存佈局,請參考《深度探索C++對象模型》一書)。github
動態內存模型可理解爲存儲一致性模型,主要是從行爲(behavioral)方面來看多個線程對同一個對象同時(讀寫)操做時(concurrency)所作的約束,動態內存模型理解起來稍微複雜一些,涉及了內存,Cache,CPU 各個層次的交互,尤爲是在共享存儲系統中,爲了保證程序執行的正確性,就須要對訪存事件施加嚴格的限制。web
文獻中常見的存儲一致性模型包括順序一致性模型,處理器一致性模型,弱一致性模型,釋放一致性模型,急切更新釋放一致性模型、懶惰更新釋放一致性模型,域一致性模型以及單項一致性模型。不一樣的存儲一致性模型對訪存事件次序的限制不一樣,於是對程序員的要求和所獲得的的性能也不同。存儲一致性模型對訪存事件次序施加的限制越弱,咱們就越有利於提升程序的性能,但編程實現上更困難。編程
順序一致性模型由 Lamport 於 1979 年提出。順序一致性模型最好理解但代價太大,原文指出:多線程
... the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program.app
該模型指出:若是在共享存儲系統中多機並行執行的結果等於把每個處理器所執行的指令流按照某種方式順序地交織在一塊兒在單機上執行的結果,則該共享存儲系統是順序一致性的。函數
順序一致性不只在共享存儲系統上適用,在多處理器和多線程環境下也一樣適用。而在多處理器和多線程環境下理解順序一致性包括兩個方面,(1). 從多個線程平行角度來看,程序最終的執行結果至關於多個線程某種交織執行的結果,(2)從單個線程內部執行順序來看,該線程中的指令是按照程序事先已規定的順序執行的(即不考慮運行時 CPU 亂序執行和 Memory Reorder)。佈局
咱們以一個具體的例子來理解順序一致性:性能
假設存在兩個共享變量a, b,初始值均爲 0,兩個線程運行不一樣的指令,以下表格所示,線程 1 設置 a 的值爲 1,而後設置 R1 的值爲 b,線程 2 設置 b 的值爲 2,並設置 R2 的值爲 a,請問在不加任何鎖或者其餘同步措施的狀況下,R1,R2 的最終結果會是多少?
線程 1 | 線程 2 |
a = 1; | b = 2; |
R1 = b; | R2 = a; |
因爲沒有施加任何同步限制,兩個線程將會交織執行,但交織執行時指令不發生重排,即線程 1 中的 a = 1 始終在 R1 = b 以前執行,而線程 2 中的 b = 2 始終在 R2 = a 以前執行 ,所以可能的執行序列共有 4!/(2!*2!) = 6 種:
狀況 1 | 狀況 2 | 狀況 3 | 狀況 4 | 狀況 5 | 狀況 6 |
a = 1;
|
b = 2;
|
a = 1;
|
a = 1;
|
b = 2;
|
b = 2;
|
R1 = b;
|
R2 = a;
|
b = 2;
|
b = 2;
|
a = 1;
|
a = 1;
|
b = 2;
|
a = 1;
|
R1 = b;
|
R2 = a;
|
R1 = b;
|
R2 = b;
|
R2 = a;
|
R1 = b;
|
R2 = a;
|
R1 = b;
|
R2 = a;
|
R1 = b;
|
R1 == 0, R2 == 1
|
R1 == 2, R2 == 0
|
R1 == 2, R2 == 1
|
R1 == 2, R2 == 1
|
R1 == 2, R2 == 1
|
R1 == 2, R2 == 1
|
上面的表格列舉了兩個線程交織執行時全部可能的執行序列,咱們發現,R1,R2 最終結果只有 3 種狀況,分別是 R1 == 0, R2 == 1(狀況 1),R1 == 2, R2 == 0(狀況2) 和 R1 == 2, R2 == 1(狀況 3, 4, 5,6)。結合上面的例子,我想你們應該理解了什麼是順序一致性。
所以,多線程環境下順序一致性包括兩個方面,(1). 從多個線程平行角度來看,程序最終的執行結果至關於多個線程某種交織執行的結果,(2)從單個線程內部執行順序來看,該線程中的指令是按照程序事先已規定的順序執行的(即不考慮運行時 CPU 亂序執行和 Memory Reorder)。
固然,順序一致性代價太大,不利於程序的優化,如今的編譯器在編譯程序時一般將指令從新排序(固然前提是保證程序的執行結果是正確的),例如,若是兩個變量讀寫互不相關,編譯器有可能將讀操做提早(暫且稱爲預讀prefetch 吧),或者儘量延遲寫操做,假設以下面的代碼段:
int a = 1, b = 2; void func() { a = b + 22; b = 22; }
在GCC 4.4 (X86-64)編譯條件下,優化選項爲 -O0 時,彙編後關鍵代碼以下:
movl b(%rip), %eax ; 將 b 讀入 %eax addl $22, %eax ; %eax 加 22, 即 b + 22 movl %eax, a(%rip) ; % 將 %eax 寫回至 a, 即 a = b + 22 movl $22, b(%rip) ; 設置 b = 22
而在設置 -O2 選項時,彙編後的關鍵代碼以下:
movl b(%rip), %eax ; 將 b 讀入 %eax movl $22, b(%rip) ; b = 22 addl $22, %eax ; %eax 加 22 movl %eax, a(%rip) ; 將 b + 22 的值寫入 a,即 a = b + 2
由上面的例子能夠看出,編譯器在不一樣的優化級別下確實對指令進行了不一樣程度重排,在 -O0(不做優化)的狀況下,彙編指令和 C 源代碼的邏輯相同,可是在 -O2 優化級別下,彙編指令和原始代碼的執行邏輯不一樣,由彙編代碼能夠觀察出,b = 22 首先執行,最後纔是 a = b + 2, 由此看出,編譯器會根據不一樣的優化等級來適當地對指令進行重排。在單線程條件下上述指令重排不會對執行結果帶來任何影響,可是在多線程環境下就不必定了。若是另一個線程依賴 a,b的值來選擇它的執行邏輯,那麼上述重排將會產生嚴重問題。編譯器優化是一門深奧的技術,可是不管編譯器怎麼優化,都須要對優化條件做出約束,尤爲是在多線程條件下,不能無理由地優化,更不能錯誤地優化。
另外,現代的 CPU 大都支持多發射和亂序執行,在亂序執行時,指令被執行的邏輯可能和程序彙編指令的邏輯不一致,在單線程條件下,CPU 的亂序執行不會帶來大問題,可是在多核多線程時代,當多線程共享某一變量時,不一樣線程對共享變量的讀寫就應該格外當心,不適當的亂序執行可能致使程序運行錯誤。所以,CPU 的亂序執行也須要做出適當的約束。
綜上所述,咱們必須對編譯器和 CPU 做出必定的約束才能合理正確地優化你的程序,那麼這個約束是什麼呢?答曰:內存模型。C++程序員要想寫出高性能的多線程程序必須理解內存模型,編譯器會給你的程序作優化(靜態),CPU爲了提高性能也有亂序執行(動態),總之,程序在最終執行時並不會按照你以前的原始代碼順序來執行,所以內存模型是程序員、編譯器,CPU 之間的契約,遵照契約後你們就各自作優化,從而儘量提升程序的性能。
C++11 中規定了 6 中訪存次序(Memory Order),以下:
enum memory_order { memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel, memory_order_seq_cst };
std::memory_order
規定了普通訪存操做和相鄰的原子訪存操做之間的次序是如何安排的,在多核系統中,當多個線程同時讀寫多個變量時,其中的某個線程所看到的變量值的改變順序可能和其餘線程寫入變量值的次序不相同。同時,不一樣的線程所觀察到的某變量被修改次序也可能不相同。然而,若是保證全部對原子變量的操做都是順序的話,可能對程序的性能影響很大,所以,咱們能夠經過 std::memory_order
來指定編譯器對訪存次序所作的限制。所以,在原子類型的 API 中,咱們能夠經過額外的參數指定該原子操做的訪存次序(內存序),默認的內存序是 std::memory_order_seq_cst。
咱們能夠把上述 6 中訪存次序(內存序)分爲 3 類,順序一致性模型(std::memory_order_seq_cst),Acquire-Release 模型(std::memory_order_consume, std::memory_order_acquire, std::memory_order_release, std::memory_order_acq_rel,) 和 Relax 模型(std::memory_order_relaxed)。三種不一樣的內存模型在不一樣類型的 CPU上(如 X86,ARM,PowerPC等)所帶來的代價也不同。例如,在 X86 或者 X86-64平臺下,Acquire-Release 類型的訪存序不須要額外的指令來保證原子性,即便順序一致性類型操做也只須要在寫操做(Store)時施加少許的限制,而在讀操做(Load)則不須要花費額外的代價來保證原子性。
===================================== TL;DR =====================================
附:本文剩餘部分將介紹其餘的存儲器一致模型中的其餘幾種較常見的模型:處理器一致性(Processor Consistency)模型,弱一致性(Weak Consistency)模型,釋放一致性(Release Consistency)模型。[注:如下內容來自中國科學院計算技術研究所胡偉武老師寫的《計算機體系結構》(清華大學出版社),該書是胡偉武老師給研究生講課所用的教材,本文略有刪改]
處理器一致性(Processor Consistency)模型:處理器一致性(Processor Consistency)模型比順序一致性模型弱,所以對於某些在順序一致性模型下可以正確執行的程序在處理器一致性條件下執行時可能會致使錯誤的結果,處理器一致性模型對訪存事件發生次序施加的限制是:(1). 在任意讀操做(Load)被容許執行以前,全部在同一處理器中先於這一 Load 的讀操做都已完成;(2). 在任意寫操做(Store)被容許執行以前,全部在同一處理器中先於這一 Store 的訪存操做(包括 Load 和 Store操做)都已完成。上述條件容許 Store 以後的 Load 越過 Store 操做而有限執行。
弱一致性(Weak Consistency)模型:弱一致性(Weak Consistency)模型的主要思想是將同步操做和普通的訪存操做區分開來,程序員必須用硬件可識別的同步操做把對可寫共享單元的訪存保護起來,以保證多個處理器對可寫單元的訪問是互斥的。弱一致性對訪存事件發生次序的限制以下:(1). 同步操做的執行知足順序一致性條件; (2). 在任一普通訪存操做被容許執行以前,全部在同一處理器中先於這一訪存操做的同步操做都已完成; (3). 在任一同步操做被容許執行以前,全部在同一處理器中先於這一同步操做的普通操做都已完成。上述條件容許在同步操做之間的普通訪存操做執行時不用考慮進程之間的相關,雖然弱一致性增長了程序員的負擔,可是它能有效地提升系統的性能。
釋放一致性(Release Consistency)模型:釋放一致性(Release Consistency)模型是對弱一致性(Weak Consistency)模型的改進,它把同步操做進一步分紅了獲取操做(Acquire)和釋放操做(Release)。Acquire 用於獲取對某些共享變量的獨佔訪問權,而 Release 則用於釋放這種訪問權,釋放一致性(Release Consistency)模型訪存事件發生次序的限制以下:(1). 同步操做的執行知足順序一致性條件; (2). 在任一普通訪存操做被容許執行以前,全部在同一處理器中先於這一訪存操做的 Acquire 操做都已完成; (3). 在任一 Release 操做被容許執行以前,全部在同一處理器中先於這一 Release 操做的普通操做都已完成。
在硬件實現的釋放一致性模型中,對共享單元的訪存是及時進行的,並在執行獲取操做(Acquire)和釋放操做(Release)時對齊。在共享虛擬存儲系統或者在由軟件維護的數據一致性的共享存儲系統中,因爲通訊和數據交換的開銷很大,有必要減小通訊和數據交換的次數。爲此,人們在釋放一致性(Release Consistency)模型的基礎上提出了急切更新釋放一致性模型(Eager Release Consistency)和懶惰更新釋放一致性模型(Lazy Release Consistency)。在急切更新釋放一致性模型中,在臨界區內的多個存數操做對共享內存的更新不是及時進行的,而是在執行 Release 操做以前(即退出臨界區以前)集中進行,把多個存數操做合併在一塊兒統一執行,從而減小了通訊次數。而在懶惰更新釋放一致性模型中,由一個處理器對某單元的存數操做並非由此處理器主動傳播到全部共享該單元的其餘處理器,而是在其餘處理器要用到此處理器所寫的數據時(即其餘處理器執行 Acquire 操做時)再向此處理器索取該單元的最新備份,這樣能夠進一步減小通訊量。
===============================================================================
好了,本文主要介紹了內存模型的相關概念,並重點介紹了順序一致性模型(附帶介紹了幾種常見的存儲一致性模型),並以一個實際的小例子向你們介紹了爲何程序員須要理解內存模型,總之,C++ 程序員要想寫出高性能的多線程程序必須理解內存模型,由於編譯器會給你的程序作優化(如指令重排等),CPU 爲了提高性能也有多發射和亂序執行,所以程序在最終執行時並不會按照你以前的原始代碼順序來執行,因此內存模型是程序員、編譯器,CPU 之間的契約,遵照契約後你們就各自作優化,從而儘量提升程序的性能。
下一節我將給你們介紹 C++11 內存模型中的 6 種訪存次序(或內存序)(std::memory_order_relaxed, std::memory_order_consume, std::memory_order_acquire, std::memory_order_release, std::memory_order_acq_rel, std::memory_order_seq_cst)各自的意義以及常見的用法,但願感興趣的同窗繼續關注,若是您發現文中的錯誤,必定儘快告訴我 ;-)
另外,後續的幾篇博客我會給你們介紹更多的與內存模型相關的知識,我在 Github 上維護了一個頁面,主要是與內存模型相關資料的連接,感興趣的同窗能夠參考裏面的資料本身閱讀。