memory order (3.1)
对应GCC的memory-order/memory-mode
Memory Barrier
内存栅栏是一个令 CPU 或编译器在内存操作上限制内存操作顺序的指令,通常意味着在 barrier 之前的指令一定在 barrier 之后的指令之前执行。
在 C11/C++11 中,引入了六种不同的 memory order,可以让程序员在并发编程中根据自己需求尽可能降低同步的粒度,以获得更好的程序性能。这六种 order 分别是:
memory_order_relaxed
memory_order_consume
memory_order_acquire
memory_order_release
memory_order_acq_rel
memory_order_seq_cst
memory_order_relaxed: 只保证当前操作的原子性,不考虑线程间的同步,其他线程可能读到新值,也可能读到旧值。比如 C++ shared_ptr 里的引用计数,我们只关心当前的应用数量,而不关心谁在引用谁在解引用。
memory_order_release:(可以理解为 mutex 的 unlock 操作)
- 对写入施加 release 语义(store),在代码中这条语句前面的所有读写操作都无法被重排到这个操作之后,即 store-store 不能重排为 store-store, load-store 也无法重排为 store-load
- 当前线程内的所有写操作,对于其他对这个原子变量进行 acquire 的线程可见
- 当前线程内的与这块内存有关的所有写操作,对于其他对这个原子变量进行 consume 的线程可见
memory_order_acquire: (可以理解为 mutex 的 lock 操作)
- 对读取施加 acquire 语义(load),在代码中这条语句后面所有读写操作都无法重排到这个操作之前,即 load-store 不能重排为 store-load, load-load 也无法重排为 load-load
- 在这个原子变量上施加 release 语义的操作发生之后,acquire 可以保证读到所有在 release 前发生的写入,举个例子:
c = 0;
thread 1:
{
a = 1;
b.store(2, memory_order_relaxed);
c.store(3, memory_order_release);
}
thread 2:
{
while (c.load(memory_order_acquire) != 3)
;
// 以下 assert 永远不会失败
assert(a == 1 && b == 2);
assert(b.load(memory_order_relaxed) == 2);
}
memory_order_consume:
- 对当前要读取的内存施加 release 语义(store),在代码中这条语句后面所有与这块内存有关的读写操作都无法被重排到这个操作之前
- 在这个原子变量上施加 release 语义的操作发生之后,consume 可以保证读到所有在 release 前发生的并且与这块内存有关的写入,举个例子:
a = 0;
c = 0;
thread 1:
{
a = 1;
c.store(3, memory_order_release);
}
thread 2:
{
while (c.load(memory_order_consume) != 3)
;
assert(a == 1); // assert 可能失败也可能不失败
}
一个简单的导致race condition
的例子
int x = 0;
//thread 1
x = 100;
//thread 2
std::cout<<x<<std::endl; //what is x ?
这个条件中可能x是0,可能是100。 可能的原因有多个:
-
thread1
落后于thread2
执行,thread2
看到的x=0
. -
thread1
在cpu0
上先于thread2
执行,thread2
在cpu1上执行,虽然它比thread1慢,但是直接从缓存里取了x=0
.
Relax mode
std::memory_order_relaxed
是最宽松的内存模型,无任何同步要求,只保证对原子变量的修改是原子的,允许编译器任意重排指令。
// 线程 1 :
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// 线程 2 :
r2 = x.load(std::memory_order_relaxed); // C
y.store(42, std::memory_order_relaxed); // D
允许出现r1 == r2 == 42
,因为在编译器和CPU的乱序执行的共同作用下,可能执行的顺序为D->A->B->C
。 Relax约束最少,适合作为无依赖的原子变量使用,比如单独的引用计数。
Acquire-Release
对同一原子变量的Acquire-Release
操作,将会影响到修改原子变量之前和之后的读写顺序。简单地说,在线程1中Release
操作之前发生的所有store
操作,在线程2Acquire
之后都保证可见。 还是拿cppreference里例子。
std::atomic<std::string*> ptr;
int data;
void producer()
{
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_acquire)))
;
assert(*p2 == "Hello"); // 绝无问题
assert(data == 42); // 绝无问题
}
虽然这里的原子变量只有ptr
,但是在ptr
的release操作之前,对int data
的写入操作, 对于consumer
acquire后的两个assert一定是可见的。
例:我们一实现一个线程读取,一个线程写入,当然,要先写入才能读取,所以这个是顺序问题。
方法一:condition_variable来操作
*
使用condition_variable,读线程wait,直到写线程调用 notify_all,即停止等待
*/
#include <thread>
#include <condition_variable>
#include <iostream>
#include <unistd.h>
using namespace std;
condition_variable g_CV;
mutex g_mtx;
int g_value=0;
void read_thread()
{
while (true)
{
unique_lock<mutex> ul(g_mtx);
g_CV.wait(ul);
cout << g_value << endl;
}
}
void write_thread()
{
while (true)
{
sleep(1);
lock_guard<mutex> lg(g_mtx);
g_value++;
g_CV.notify_all();
}
}
int main()
{
thread th(read_thread);
th.detach();
thread th2(write_thread);
th2.detach();
char a;
while (cin >> a);
return 0;
}
方法二:使用标准原子操作
/*使用原子操作来控制线程顺序,实现了一个类似于互斥锁这么一个概念*/
#include <thread>
#include <atomic>
#include <iostream>
#include <unistd.h>
using namespace std;
int g_value(0);
atomic<bool> g_data_ready(false);
void read_thread()
{
while (true)
{
while (!g_data_ready.load());//用于控制进入
cout << g_value << endl;
g_data_ready = false;
}
}
void write_thread()
{
while (true)
{
while (g_data_ready.load());//用于控制进入
sleep(1);
g_value++;
g_data_ready = true;
}
}
int main()
{
thread th(read_thread);
th.detach();
thread th2(write_thread);
th2.detach();
char a;
while (cin >> a);
return 0;
}
~
root@ubuntu:~/c++# g++ -std=c++11 -pthread atom2.cpp -o atom
root@ubuntu:~/c++# ./atom
1
2
3
4
5
^C
root@ubuntu:~/c++#
六种原子操作内存顺序
这个比较的头痛了,对于刚接触这个概念的时候,我也是一头雾水。。但随着我各种查资料,慢慢就有所理解,知乎的一位答主讲的比较不错,点此,再加之深究《C++并发编程实战》第五章的内容。
//这里枚举这六种
typedef enum memory_order {
memory_order_relaxed,
memory_order_consume,
memory_order_acquire,
memory_order_release,
memory_order_acq_rel,
memory_order_seq_cst
} m
- memory_order_seq_cst :这个是默认的原子顺序,即按代码怎么写的就是怎么个顺序!
- memory_order_relaxed:这个是松散顺序,《C++并发编程实战》第5章 123页举的例子讲的很清楚,鉴于篇幅,我也简单陈述一下,举书本上的例子:
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x,y;
std::atomic<int> z;
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()
{
x=false;
y=false;
z=0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load()!=0); //5//断言发生,z是可能等于0的
}
root@ubuntu:~/c++# g++ -std=c++11 -pthread atom3.cpp -o atom
root@ubuntu:~/c++# ./atom
root@ubuntu:~/c++# ./atom
root@ubuntu:~/c++# ./atom
root@ubuntu:~/c++# ./atom
为什么断言可能发生?意思是z可能为0,x可能为0,这个问题,就是relaxed的锅, 在write_x_then_y线程中,因为#1,#2的store是松散的,在read_y_then_x线程中,也是以松散来读的,x与y没有必然的联系,意思是x.load的时候,可能返回false,编译器或者硬件可随便更改线程中的顺序,所以说慎用使用松散顺序!还有就是这种是理想条件下的,至少x86Cpu目前没有该功能!
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x,y;
std::atomic<int> z;
void write_x()
{
x.store(true,std::memory_order_release);
}
void write_y()
{
y.store(true,std::memory_order_release);
}
void read_x_then_y()
{
while(!x.load(std::memory_order_acquire));
if(y.load(std::memory_order_acquire))//y为false;
++z;
}
void read_y_then_x()
{
while(!y.load(std::memory_order_acquire));
if(x.load(std::memory_order_acquire))//x为false;
++z;
}
int main()
{
x=false;
y=false;
z=0;
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y);
std::thread d(read_y_then_x);
a.join();
b.join();
c.join();
d.join();
assert(z.load()!=0);
}
root@ubuntu:~/c++# g++ -std=c++11 -pthread require.cpp -o require
root@ubuntu:~/c++# ./require
root@ubuntu:~/c++#