3 回答

TA貢獻1848條經(jīng)驗 獲得超6個贊
首先,你必須學會像語言律師那樣思考。
C ++規(guī)范不引用任何特定的編譯器,操作系統(tǒng)或CPU。它引用了一個抽象機器,它是實際系統(tǒng)的概括。在語言律師的世界里,程序員的工作就是為抽象機器編寫代碼; 編譯器的工作是在具體機器上實現(xiàn)該代碼。通過嚴格按照規(guī)范進行編碼,無論是今天還是50年后,您都可以確定您的代碼無需在具有兼容C ++編譯器的任何系統(tǒng)上進行編譯和運行。
C ++ 98 / C ++ 03規(guī)范中的抽象機器基本上是單線程的。所以不可能編寫相對于規(guī)范“完全可移植”的多線程C ++代碼。該規(guī)范甚至沒有說明內(nèi)存加載和存儲的原子性或加載和存儲可能發(fā)生的順序,更不用說像互斥體這樣的東西了。
當然,您可以在實踐中為特定的具體系統(tǒng)編寫多線程代碼 - 例如pthreads或Windows。但是沒有標準的方法來為C ++ 98 / C ++ 03編寫多線程代碼。
C ++ 11中的抽象機器是設(shè)計多線程的。它還有一個定義明確的內(nèi)存模型 ; 也就是說,它說明了在訪問內(nèi)存時編譯器可能會做什么,也可能不會做什么。
請考慮以下示例,其中兩個線程同時訪問一對全局變量:
Global int x, y;Thread 1 Thread 2x = 17; cout << y << " ";y = 37; cout << x << endl;
線程2可能輸出什么?
在C ++ 98 / C ++ 03下,這甚至不是Undefined Behavior; 問題本身毫無意義,因為標準沒有考慮任何稱為“線程”的東西。
在C ++ 11下,結(jié)果是Undefined Behavior,因為加載和存儲通常不需要是原子的。這可能看起來不是很大的改善......而且它本身并非如此。
但是使用C ++ 11,你可以這樣寫:
Global atomic<int> x, y;Thread 1 Thread 2x.store(17); cout << y.load() << " ";y.store(37); cout << x.load() << endl;
現(xiàn)在情況變得更有趣了。首先,定義了此處的行為。線程2現(xiàn)在可以打印0 0
(如果它在線程1之前運行),37 17
(如果它在線程1之后運行),或者0 17
(如果它在線程1分配給x但在它分配給y之前運行)。
它無法打印的是37 0
,因為C ++ 11中原子加載/存儲的默認模式是強制執(zhí)行順序一致性。這只意味著所有加載和存儲必須“好像”它們按照您在每個線程中編寫它們的順序發(fā)生,而線程之間的操作可以交錯,但系統(tǒng)喜歡。因此,atomics的默認行為為加載和存儲提供了原子性和排序。
現(xiàn)在,在現(xiàn)代CPU上,確保順序一致性可能很昂貴。特別是,編譯器可能會在每次訪問之間發(fā)出完整的內(nèi)存屏障。但是,如果您的算法可以容忍無序的加載和存儲; 即,如果它需要原子性而不是訂購; 即,如果它可以容忍37 0
這個程序的輸出,那么你可以這樣寫:
Global atomic<int> x, y;Thread 1 Thread 2x.store(17,memory_order_relaxed); cout << y.load(memory_order_relaxed) << " ";y.store(37,memory_order_relaxed); cout << x.load(memory_order_relaxed) << endl;
CPU越現(xiàn)代,就越有可能比前一個例子更快。
最后,如果您只需要按順序保持特定的加載和存儲,您可以編寫:
Global atomic<int> x, y;Thread 1 Thread 2x.store(17,memory_order_release); cout << y.load(memory_order_acquire) << " ";y.store(37,memory_order_release); cout << x.load(memory_order_acquire) << endl;
這將我們帶回有序的加載和存儲 - 因此37 0
不再是可能的輸出 - 但它以最小的開銷實現(xiàn)了這一點。(在這個簡單的例子中,結(jié)果與完整的順序一致性相同;在較大的程序中,它不會。)
當然,如果您想要查看的唯一輸出是0 0
或者37 17
,您可以在原始代碼周圍包裝互斥鎖。但是如果你已經(jīng)讀過這篇文章了,我打賭你已經(jīng)知道它是如何工作的,這個答案已經(jīng)比我預想的要長:-)。
所以,底線。互斥體很棒,C ++ 11將它們標準化。但有時出于性能原因,您需要較低級別的基元(例如,經(jīng)典的雙重檢查鎖定模式)。新標準提供了高級小工具,如互斥鎖和條件變量,它還提供低級小工具,如原子類型和各種內(nèi)存屏障。因此,現(xiàn)在您可以完全使用標準指定的語言編寫復雜的高性能并發(fā)例程,并且可以確定您的代碼將在今天的系統(tǒng)和未來的系統(tǒng)上編譯和運行。
雖然坦率地說,除非您是專家并且正在處理一些嚴重的低級代碼,否則您應該堅持使用互斥鎖和條件變量。這就是我打算做的事情。
有關(guān)這些內(nèi)容的更多信息,請參閱此博客文章。

TA貢獻1757條經(jīng)驗 獲得超7個贊
這是一個多年前的問題,但是非常受歡迎,值得一提的是學習C ++ 11內(nèi)存模型的絕佳資源。我認為總結(jié)他的演講是沒有意義的,以便再做一個完整的答案,但鑒于這是實際編寫標準的人,我認為值得觀看談話。
Herb Sutter有一個長達3個小時的關(guān)于C ++ 11內(nèi)存模型的討論,名為“atomic <> Weapons”,可在Channel9網(wǎng)站上找到 - 第1 部分和第2部分。這個講座非常技術(shù)性,涵蓋以下主題:
優(yōu)化,種族和記憶模型
訂購 - 什么:獲取和發(fā)布
訂購 - 如何:互斥鎖,原子和/或柵欄
編譯器和硬件的其他限制
代碼和性能:x86 / x64,IA64,POWER,ARM
輕松的原子論
談話沒有詳細說明API,而是關(guān)于推理,背景,幕后和幕后(您是否知道輕松的語義被添加到標準中只是因為POWER和ARM不能有效地支持同步加載?)。
- 3 回答
- 0 關(guān)注
- 645 瀏覽
添加回答
舉報