C++11 并发指南七(C++11 内存模型一:介绍)
第六章主要介紹了 C++11 中的原子類型及其相關(guān)的API,原子類型的大多數(shù) API 都需要程序員提供一個(gè) std::memory_order(可譯為內(nèi)存序,訪存順序) 的枚舉類型值作為參數(shù),比如:atomic_store,atomic_load,atomic_exchange,atomic_compare_exchange 等 API 的最后一個(gè)形參為 std::memory_order order,默認(rèn)值是 std::memory_order_seq_cst(順序一致性)。那么究竟什么是 std::memory_order 呢,為了解答這個(gè)問題,我們先來討論 C++11 的內(nèi)存模型。
一般來講,內(nèi)存模型可分為靜態(tài)內(nèi)存模型和動(dòng)態(tài)內(nèi)存模型,靜態(tài)內(nèi)存模型主要涉及類的對(duì)象在內(nèi)存中是如何存放的,即從結(jié)構(gòu)(structural)方面來看一個(gè)對(duì)象在內(nèi)存中的布局,以一個(gè)簡(jiǎn)單的例子為例(截圖參考《C++? Concurrency In Action》 P105 ):
上面是一個(gè)簡(jiǎn)單的 C++ 類(又稱POD: Plain Old Data,它沒有虛函數(shù),沒有繼承),它在內(nèi)存中的布局如圖右邊所示(對(duì)于復(fù)雜類對(duì)象的內(nèi)存布局,請(qǐng)參考《深度探索C++對(duì)象模型》一書)。
動(dòng)態(tài)內(nèi)存模型可理解為存儲(chǔ)一致性模型,主要是從行為(behavioral)方面來看多個(gè)線程對(duì)同一個(gè)對(duì)象同時(shí)(讀寫)操作時(shí)(concurrency)所做的約束,動(dòng)態(tài)內(nèi)存模型理解起來稍微復(fù)雜一些,涉及了內(nèi)存,Cache,CPU 各個(gè)層次的交互,尤其是在共享存儲(chǔ)系統(tǒng)中,為了保證程序執(zhí)行的正確性,就需要對(duì)訪存事件施加嚴(yán)格的限制。
文獻(xiàn)中常見的存儲(chǔ)一致性模型包括順序一致性模型,處理器一致性模型,弱一致性模型,釋放一致性模型,急切更新釋放一致性模型、懶惰更新釋放一致性模型,域一致性模型以及單項(xiàng)一致性模型。不同的存儲(chǔ)一致性模型對(duì)訪存事件次序的限制不同,因而對(duì)程序員的要求和所得到的的性能也不一樣。存儲(chǔ)一致性模型對(duì)訪存事件次序施加的限制越弱,我們就越有利于提高程序的性能,但編程實(shí)現(xiàn)上更困難。
順序一致性模型由 Lamport 于 1979 年提出。順序一致性模型最好理解但代價(jià)太大,原文指出:
... 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.
該模型指出:如果在共享存儲(chǔ)系統(tǒng)中多機(jī)并行執(zhí)行的結(jié)果等于把每一個(gè)處理器所執(zhí)行的指令流按照某種方式順序地交織在一起在單機(jī)上執(zhí)行的結(jié)果,則該共享存儲(chǔ)系統(tǒng)是順序一致性的。
順序一致性不僅在共享存儲(chǔ)系統(tǒng)上適用,在多處理器和多線程環(huán)境下也同樣適用。而在多處理器和多線程環(huán)境下理解順序一致性包括兩個(gè)方面,(1). 從多個(gè)線程平行角度來看,程序最終的執(zhí)行結(jié)果相當(dāng)于多個(gè)線程某種交織執(zhí)行的結(jié)果,(2)從單個(gè)線程內(nèi)部執(zhí)行順序來看,該線程中的指令是按照程序事先已規(guī)定的順序執(zhí)行的(即不考慮運(yùn)行時(shí) CPU 亂序執(zhí)行和 Memory Reorder)。
我們以一個(gè)具體的例子來理解順序一致性:
假設(shè)存在兩個(gè)共享變量a, b,初始值均為 0,兩個(gè)線程運(yùn)行不同的指令,如下表格所示,線程 1 設(shè)置 a 的值為 1,然后設(shè)置 R1 的值為 b,線程 2 設(shè)置 b 的值為 2,并設(shè)置 R2 的值為 a,請(qǐng)問在不加任何鎖或者其他同步措施的情況下,R1,R2 的最終結(jié)果會(huì)是多少?
?
| 線程 1 | 線程 2 |
| a = 1; | b = 2; |
| R1 = b; | R2 = a; |
?
由于沒有施加任何同步限制,兩個(gè)線程將會(huì)交織執(zhí)行,但交織執(zhí)行時(shí)指令不發(fā)生重排,即線程 1 中的 a = 1 始終在 R1 = b 之前執(zhí)行,而線程 2 中的 b = 2 始終在 R2 = a 之前執(zhí)行 ,因此可能的執(zhí)行序列共有 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 |
?
上面的表格列舉了兩個(gè)線程交織執(zhí)行時(shí)所有可能的執(zhí)行序列,我們發(fā)現(xiàn),R1,R2 最終結(jié)果只有 3 種情況,分別是 R1 == 0, R2 == 1(情況 1),R1 == 2, R2 == 0(情況2) 和 R1 == 2, R2 == 1(情況 3, 4, 5,6)。結(jié)合上面的例子,我想大家應(yīng)該理解了什么是順序一致性。
因此,多線程環(huán)境下順序一致性包括兩個(gè)方面,(1). 從多個(gè)線程平行角度來看,程序最終的執(zhí)行結(jié)果相當(dāng)于多個(gè)線程某種交織執(zhí)行的結(jié)果,(2)從單個(gè)線程內(nèi)部執(zhí)行順序來看,該線程中的指令是按照程序事先已規(guī)定的順序執(zhí)行的(即不考慮運(yùn)行時(shí) CPU 亂序執(zhí)行和 Memory Reorder)。
當(dāng)然,順序一致性代價(jià)太大,不利于程序的優(yōu)化,現(xiàn)在的編譯器在編譯程序時(shí)通常將指令重新排序(當(dāng)然前提是保證程序的執(zhí)行結(jié)果是正確的),例如,如果兩個(gè)變量讀寫互不相關(guān),編譯器有可能將讀操作提前(暫且稱為預(yù)讀prefetch 吧),或者盡可能延遲寫操作,假設(shè)如下面的代碼段:
int a = 1, b = 2;void func() {a = b + 22;b = 22; }?在GCC 4.4 (X86-64)編譯條件下,優(yōu)化選項(xiàng)為 -O0 時(shí),匯編后關(guān)鍵代碼如下:
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) ; 設(shè)置 b = 22而在設(shè)置 -O2 選項(xiàng)時(shí),匯編后的關(guān)鍵代碼如下:
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由上面的例子可以看出,編譯器在不同的優(yōu)化級(jí)別下確實(shí)對(duì)指令進(jìn)行了不同程度重排,在 -O0(不作優(yōu)化)的情況下,匯編指令和 C 源代碼的邏輯相同,但是在 -O2 優(yōu)化級(jí)別下,匯編指令和原始代碼的執(zhí)行邏輯不同,由匯編代碼可以觀察出,b = 22 首先執(zhí)行,最后才是 a = b + 2, 由此看出,編譯器會(huì)根據(jù)不同的優(yōu)化等級(jí)來適當(dāng)?shù)貙?duì)指令進(jìn)行重排。在單線程條件下上述指令重排不會(huì)對(duì)執(zhí)行結(jié)果帶來任何影響,但是在多線程環(huán)境下就不一定了。如果另外一個(gè)線程依賴 a,b的值來選擇它的執(zhí)行邏輯,那么上述重排將會(huì)產(chǎn)生嚴(yán)重問題。編譯器優(yōu)化是一門深?yuàn)W的技術(shù),但是無論編譯器怎么優(yōu)化,都需要對(duì)優(yōu)化條件作出約束,尤其是在多線程條件下,不能無理由地優(yōu)化,更不能錯(cuò)誤地優(yōu)化。
另外,現(xiàn)代的 CPU 大都支持多發(fā)射和亂序執(zhí)行,在亂序執(zhí)行時(shí),指令被執(zhí)行的邏輯可能和程序匯編指令的邏輯不一致,在單線程條件下,CPU 的亂序執(zhí)行不會(huì)帶來大問題,但是在多核多線程時(shí)代,當(dāng)多線程共享某一變量時(shí),不同線程對(duì)共享變量的讀寫就應(yīng)該格外小心,不適當(dāng)?shù)膩y序執(zhí)行可能導(dǎo)致程序運(yùn)行錯(cuò)誤。因此,CPU 的亂序執(zhí)行也需要作出適當(dāng)?shù)募s束。
綜上所述,我們必須對(duì)編譯器和 CPU 作出一定的約束才能合理正確地優(yōu)化你的程序,那么這個(gè)約束是什么呢?答曰:內(nèi)存模型。C++程序員要想寫出高性能的多線程程序必須理解內(nèi)存模型,編譯器會(huì)給你的程序做優(yōu)化(靜態(tài)),CPU為了提升性能也有亂序執(zhí)行(動(dòng)態(tài)),總之,程序在最終執(zhí)行時(shí)并不會(huì)按照你之前的原始代碼順序來執(zhí)行,因此內(nèi)存模型是程序員、編譯器,CPU 之間的契約,遵守契約后大家就各自做優(yōu)化,從而盡可能提高程序的性能。
C++11 中規(guī)定了 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 規(guī)定了普通訪存操作和相鄰的原子訪存操作之間的次序是如何安排的,在多核系統(tǒng)中,當(dāng)多個(gè)線程同時(shí)讀寫多個(gè)變量時(shí),其中的某個(gè)線程所看到的變量值的改變順序可能和其他線程寫入變量值的次序不相同。同時(shí),不同的線程所觀察到的某變量被修改次序也可能不相同。然而,如果保證所有對(duì)原子變量的操作都是順序的話,可能對(duì)程序的性能影響很大,因此,我們可以通過std::memory_order 來指定編譯器對(duì)訪存次序所做的限制。因此,在原子類型的 API 中,我們可以通過額外的參數(shù)指定該原子操作的訪存次序(內(nèi)存序),默認(rèn)的內(nèi)存序是std::memory_order_seq_cst。
我們可以把上述 6 中訪存次序(內(nèi)存序)分為 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)。三種不同的內(nèi)存模型在不同類型的 CPU上(如 X86,ARM,PowerPC等)所帶來的代價(jià)也不一樣。例如,在 X86 或者 X86-64平臺(tái)下,Acquire-Release 類型的訪存序不需要額外的指令來保證原子性,即使順序一致性類型操作也只需要在寫操作(Store)時(shí)施加少量的限制,而在讀操作(Load)則不需要花費(fèi)額外的代價(jià)來保證原子性。
=====================================?TL;DR =====================================
附:本文剩余部分將介紹其他的存儲(chǔ)器一致模型中的其他幾種較常見的模型:處理器一致性(Processor Consistency)模型,弱一致性(Weak Consistency)模型,釋放一致性(Release Consistency)模型。[注:以下內(nèi)容來自中國科學(xué)院計(jì)算技術(shù)研究所胡偉武老師寫的《計(jì)算機(jī)體系結(jié)構(gòu)》(清華大學(xué)出版社),該書是胡偉武老師給研究生講課所用的教材,本文略有刪改]
處理器一致性(Processor Consistency)模型:處理器一致性(Processor Consistency)模型比順序一致性模型弱,因此對(duì)于某些在順序一致性模型下能夠正確執(zhí)行的程序在處理器一致性條件下執(zhí)行時(shí)可能會(huì)導(dǎo)致錯(cuò)誤的結(jié)果,處理器一致性模型對(duì)訪存事件發(fā)生次序施加的限制是:(1). 在任意讀操作(Load)被允許執(zhí)行之前,所有在同一處理器中先于這一 Load 的讀操作都已完成;(2). 在任意寫操作(Store)被允許執(zhí)行之前,所有在同一處理器中先于這一 Store 的訪存操作(包括 Load 和 Store操作)都已完成。上述條件允許 Store 之后的 Load 越過 Store 操作而有限執(zhí)行。
弱一致性(Weak Consistency)模型:弱一致性(Weak Consistency)模型的主要思想是將同步操作和普通的訪存操作區(qū)分開來,程序員必須用硬件可識(shí)別的同步操作把對(duì)可寫共享單元的訪存保護(hù)起來,以保證多個(gè)處理器對(duì)可寫單元的訪問是互斥的。弱一致性對(duì)訪存事件發(fā)生次序的限制如下:(1). 同步操作的執(zhí)行滿足順序一致性條件; (2). 在任一普通訪存操作被允許執(zhí)行之前,所有在同一處理器中先于這一訪存操作的同步操作都已完成; (3). 在任一同步操作被允許執(zhí)行之前,所有在同一處理器中先于這一同步操作的普通操作都已完成。上述條件允許在同步操作之間的普通訪存操作執(zhí)行時(shí)不用考慮進(jìn)程之間的相關(guān),雖然弱一致性增加了程序員的負(fù)擔(dān),但是它能有效地提高系統(tǒng)的性能。
釋放一致性(Release Consistency)模型:釋放一致性(Release Consistency)模型是對(duì)弱一致性(Weak Consistency)模型的改進(jìn),它把同步操作進(jìn)一步分成了獲取操作(Acquire)和釋放操作(Release)。Acquire 用于獲取對(duì)某些共享變量的獨(dú)占訪問權(quán),而 Release 則用于釋放這種訪問權(quán),釋放一致性(Release Consistency)模型訪存事件發(fā)生次序的限制如下:(1).?同步操作的執(zhí)行滿足順序一致性條件; (2). 在任一普通訪存操作被允許執(zhí)行之前,所有在同一處理器中先于這一訪存操作的 Acquire 操作都已完成; (3). 在任一 Release 操作被允許執(zhí)行之前,所有在同一處理器中先于這一 Release 操作的普通操作都已完成。
在硬件實(shí)現(xiàn)的釋放一致性模型中,對(duì)共享單元的訪存是及時(shí)進(jìn)行的,并在執(zhí)行獲取操作(Acquire)和釋放操作(Release)時(shí)對(duì)齊。在共享虛擬存儲(chǔ)系統(tǒng)或者在由軟件維護(hù)的數(shù)據(jù)一致性的共享存儲(chǔ)系統(tǒng)中,由于通信和數(shù)據(jù)交換的開銷很大,有必要減少通信和數(shù)據(jù)交換的次數(shù)。為此,人們?cè)卺尫乓恢滦?Release Consistency)模型的基礎(chǔ)上提出了急切更新釋放一致性模型(Eager Release Consistency)和懶惰更新釋放一致性模型(Lazy Release Consistency)。在急切更新釋放一致性模型中,在臨界區(qū)內(nèi)的多個(gè)存數(shù)操作對(duì)共享內(nèi)存的更新不是及時(shí)進(jìn)行的,而是在執(zhí)行 Release 操作之前(即退出臨界區(qū)之前)集中進(jìn)行,把多個(gè)存數(shù)操作合并在一起統(tǒng)一執(zhí)行,從而減少了通信次數(shù)。而在懶惰更新釋放一致性模型中,由一個(gè)處理器對(duì)某單元的存數(shù)操作并不是由此處理器主動(dòng)傳播到所有共享該單元的其他處理器,而是在其他處理器要用到此處理器所寫的數(shù)據(jù)時(shí)(即其他處理器執(zhí)行 Acquire 操作時(shí))再向此處理器索取該單元的最新備份,這樣可以進(jìn)一步減少通信量。
===============================================================================
好了,本文主要介紹了內(nèi)存模型的相關(guān)概念,并重點(diǎn)介紹了順序一致性模型(附帶介紹了幾種常見的存儲(chǔ)一致性模型),并以一個(gè)實(shí)際的小例子向大家介紹了為什么程序員需要理解內(nèi)存模型,總之,C++ 程序員要想寫出高性能的多線程程序必須理解內(nèi)存模型,因?yàn)榫幾g器會(huì)給你的程序做優(yōu)化(如指令重排等),CPU 為了提升性能也有多發(fā)射和亂序執(zhí)行,因此程序在最終執(zhí)行時(shí)并不會(huì)按照你之前的原始代碼順序來執(zhí)行,所以內(nèi)存模型是程序員、編譯器,CPU 之間的契約,遵守契約后大家就各自做優(yōu)化,從而盡可能提高程序的性能。
下一節(jié)我將給大家介紹 C++11 內(nèi)存模型中的 6 種訪存次序(或內(nèi)存序)(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)各自的意義以及常見的用法,希望感興趣的同學(xué)繼續(xù)關(guān)注,如果您發(fā)現(xiàn)文中的錯(cuò)誤,一定盡快告訴我 ;-)
另外,后續(xù)的幾篇博客我會(huì)給大家介紹更多的與內(nèi)存模型相關(guān)的知識(shí),我在 Github 上維護(hù)了一個(gè)頁面,主要是與內(nèi)存模型相關(guān)資料的鏈接,感興趣的同學(xué)可以參考里面的資料自己閱讀。
總結(jié)
以上是生活随笔為你收集整理的C++11 并发指南七(C++11 内存模型一:介绍)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ArcGIS字段计算器 Field Ca
- 下一篇: C++中友元