[TOC]
參考
1. C++11多線程-內(nèi)存模型
2. c++并發(fā)編程1.內(nèi)存序
3. 淺談Memory Reordering
4. C++11中的內(nèi)存模型下篇 - C++11支持的幾種內(nèi)存模型
5. C++11中的內(nèi)存模型上篇 - 內(nèi)存模型基礎
前言
有三種情況她肯,可能導致亂序執(zhí)行:編譯器優(yōu)化、CPU亂序、緩存不一致居夹。進而導致多線程情況下出現(xiàn)問題陨享。[1,3,4]
c++11引入了atomic類型之后,大大方便了原子變量的使用吭狡,但是原子變量的內(nèi)存序有好幾種囤锉,這又引入了讓人難以理解的內(nèi)容。
內(nèi)存序分為三類六種
- relaxed(松弛的內(nèi)存序)
- sequential_consistency(內(nèi)存一致序)
- acquire-release(獲取-釋放一致性)
relaxed
//test.cpp
#include <thread>
#include <atomic>
#include <assert.h>
std::atomic<bool> x{false},y{false};
std::atomic<int> z{0};
void write_x_then_y() {
x.store(true,std::memory_order_relaxed); //1
y.store(true,std::memory_order_relaxed); //2
}
void read_y_then_x() {
while(!y.load(std::memory_order_relaxed)); //3
if(x.load(std::memory_order_relaxed)) //4
++z;
}
int main() {
std::thread b(read_y_then_x);
std::thread a(write_x_then_y);
a.join();
b.join();
if (z.load() != 0) return 0; else return 1;
}
# test.sh
#!/bin/bash
for ((i=0;i<1;)); do
./a.out
if [ "$?" == "1" ];then
break
fi
done
g++ -std=c++17 -pthread -O2 test.cpp
編譯以上代碼涡拘,time sh test.sh
執(zhí)行代碼玲躯。
如果出現(xiàn) 2 -> 3 -> 4 -> 1這樣的執(zhí)行次序,那么就會出現(xiàn)z == 0
這種錯誤情況鳄乏。
<font color=red>注跷车,不過我跑了一晚上,并沒有復現(xiàn)這個結(jié)果</font>
那么relaxed用于何處呢橱野?對于計數(shù)這種場景朽缴,就可以使用relaxed來最大化性能。
#include <cassert>
#include <vector>
#include <thread>
#include <atomic>
std::atomic<int> count{0};
void f() {
for (int n = 0; n < 1000; ++n) {
count.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::thread threads[10];
for (std::thread &thr: threads) {
thr = std::thread(f);
}
for (auto &thr : v) {
thr.join();
}
assert(cnt == 10000); // 永遠不會失敗
return 0;
}
release-acquire
針對relaxed的例子水援,如果改成如下的代碼就可以避免z == 0
這種錯誤情況密强。
#include <thread>
#include <atomic>
#include <assert.h>
std::atomic<bool> x{false},y{false};
std::atomic<int> z{0};
void write_x_then_y() {
x.store(true,std::memory_order_relaxed); //1
y.store(true,std::memory_order_release); //2
}
void read_y_then_x() {
while(!y.load(std::memory_order_acquire)); //3
if(x.load(std::memory_order_relaxed)) //4
++z;
}
int main() {
std::thread b(read_y_then_x);
std::thread a(write_x_then_y);
a.join();
b.join();
if (z.load() != 0) return 0; else return 1;
}
他會保證1發(fā)生在2前茅郎,4發(fā)生在3后,同時3一定發(fā)生在2后或渤,那么z == 0
不會發(fā)生系冗。
如下圖分析
- 初始條件為x = y = false。
- 在write_x_then_y線程中薪鹦,先執(zhí)行對x的寫操作掌敬,再執(zhí)行對y的寫操作,由于兩者在同一個線程中池磁,所以即便針對x的修改操作使用relaxed模型奔害,修改x也一定在修改y之前執(zhí)行。
- 在write_x_then_y線程中地熄,對y的load操作使用了acquire模型华临,而在線程write_x_then_y中針對變量y的讀操作使用release模型,因此保證了是先執(zhí)行write_x_then_y函數(shù)才到read_y_then_x的針對變量y的load操作端考。
- 因此最終的執(zhí)行順序如上圖所示雅潭,此時不可能出現(xiàn)z=0的情況。
從以上的分析可以看出却特,針對同一個變量的release-acquire操作寻馏,更多時候扮演了一種“線程間使用某一變量的同步”作用,由于有了這個語義的保證核偿,做到了線程間操作的先后順序保證(inter-thread happens-before)。
可以簡單記作release為寫不后
顽染,acquire為讀不前
漾岳。[2]
release-consume
官方不推薦,此處不進行詳細描述粉寞。簡單說尼荆,release-acquire會把不相關(guān)的變量存取都進行保序,release-consume只會對有依賴的變量保序唧垦,進而提高效率捅儒,同時也使代碼更容易引入bug。
sequential consistency
這是最嚴格的級別振亮,也是性能最差的級別巧还,同時也是默認的級別。
如下列:
#include <thread>
#include <atomic>
#include <cassert>
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
void write_x() {
x.store(true, std::memory_order_seq_cst); // 1
}
void write_y() {
y.store(true, std::memory_order_seq_cst); // 2
}
void read_x_then_y() {
while (!x.load(std::memory_order_seq_cst)); // 3
if (y.load(std::memory_order_seq_cst)) { // 4
++z;
}
}
void read_y_then_x() {
while (!y.load(std::memory_order_seq_cst)); // 5
if (x.load(std::memory_order_seq_cst)) { // 6
++z;
}
}
int main() {
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y); // thread c
std::thread d(read_y_then_x); // thread d
a.join(); b.join(); c.join(); d.join();
// failed to assert without memory_order_seq_cst
assert(z.load() != 0);
}
如果使用release-acquire坊秸,那么線程c可能看到的是2 -> 1這個執(zhí)行順序麸祷,但是線程d可能看到的是1->2這個執(zhí)行順序,進而導致z == 0
褒搔。
如下圖分析:
- 初始條件為x = y = false阶牍。
- 由于在read_x_and_y線程中喷面,對x的load操作使用了acquire模型,因此保證了是先執(zhí)行write_x函數(shù)才到這一步的走孽;同理先執(zhí)行write_y才到read_y_and_x中針對y的load操作惧辈。
- 然而即便如此,也可能出現(xiàn)在read_x_then_y中針對y的load操作在y的store操作之前完成磕瓷,因為y.store操作與此之間沒有先后順序關(guān)系盒齿;同理也不能保證x一定讀到true值,因此到程序結(jié)束是就出現(xiàn)了z = 0的情況生宛。
從上面的分析可以看到县昂,即便在這里使用了release-acquire模型,仍然沒有保證z==0陷舅,其原因在于:最開始針對x倒彰、y兩個變量的寫操作是分別在write_x和write_y線程中進行的,不能保證兩者執(zhí)行的順序?qū)е隆?/p>