一、memory order产生的历史,用来解决什么问题?
在多核编程中,我们使用锁来避免多个线程修改同一个数据时产生的竞争条件。但是,锁会消耗系统资源,当锁成为性能瓶颈的时候,就需要使用另一种方法——原子类型。c++11中引入了原子类型atomic。
但仅靠原子类型实现不了对资源的访问控制。这造成的原因是编译器和CPU(乱序执行)对单个线程内的指令流实施了指令重排,只要不存在依赖,代码中后面的指令可能会被放在前面,从而先执行它——这将导致多线程间对共享数据的读写顺序会发生变化。cpu这么做是为了尽量塞满每个时钟周期,在单位时间内尽量执行更多的指令,从而提高吞吐率。但对程序本身而言,却是致命的,导致了诸多不稳定性和BUG。memory order的出现就是为了解决多核编程中,由于编译器和CPU对单线程内的指令流实施指令重排导致多线程间对共享数据的读写顺序发生变化,引起多线程程序执行状态异常的问题。
二、C++ 11中的原子类型变量如何使用?
假设x为std::atomic类型的原子变量:
(1) x.load()返回x的值。
(2) x.store(n)把x设为n,什么都不返回。
(3) x.exchange(n)把x设为n,返回设定之前的值。
(4) x.compare_exchange_strong(expected_ref, desired)若x等于expected_ref,则设为desired, 返回成功;否则把最新值写入expected_ref,返回失败。
(5) x.compare_exchange_weak(expected_ref, desired)相比compare_exchange_strong可能有spurious wakeup。x.fetch_add(n), x.fetch_sub(n)原子地做x += n, x-= n,返回修改之前的值。
三、C++ 11中memory order的类型
memory order | 作用 |
memory_order_relaxed | 无fencing作用,cpu和编译器可以重排指令 |
memory_order_consume | 后面依赖此原子变量的访存指令勿重排至此条指令之前 |
memory_order_acquire | 后面访存指令勿重排至此条指令之前 |
memory_order_release | 前面的访存指令勿排到此条指令之后。当此条指令的结果被同步到其他核的cache中时,保证前面的指令也已经被同步。 |
memory_order_acq_rel | acquare + release |
memory_order_seq_cst | acq_rel + 所有使用seq_cst的指令有严格的全序关系 |
需要注意的是,上述memory order类型需要结合atomic类型变量,让用户可以在多线程之间实现对某个原子变量的有序读写。
四、C++ 11中memory order的应用案例
// thread 1
// ready was initialized to false
p.init();
ready = true;
// thread 2
if(ready) {
p.bar();
}
线程2在ready为true的时候会访问p,对线程1来说,如果按照正常的执行顺序,那么p先被初始化,然后在将ready赋为true。但对多核的机器而言,情况可能有所变化:
• 线程1中的ready = true可能会被cpu或编译器重排到p.init()的前面,从而优先执行ready = true这条指令。在线程2中,p.bar()中的一些代码可能被重排到if(ready)之前。
• 即使没有重排,ready和p的值也会独立地同步到线程2所在核心的cache,线程2仍然可能在看到ready为true时看到未初始化的p。
为了解决这个问题,cpu和编译器提供了memory fence,让用户可以声明针对某个原子变量访存指令的可见性关系,从而实现多线程间对原子变量的有序读写。
修改后的例子入下:
// Thread1
// ready was initialized to false
p.init();
ready.store(true, std::memory_order_release);
// Thread2
if (ready.load(std::memory_order_acquire)) {
p.bar();
}
线程2中的acquire和线程1的release配对,确保线程2在看到ready==true时能看到线程1 release之前所有的访存操作。
需要注意的是:memory fence不等于可见性,即使线程2恰好在线程1在把ready设置为true后读取了ready也不意味着它能看到true,因为同步cache是有延时的。memory fence保证的是可见性的顺序:“假如我看到了a的最新值,那么我一定也得看到b的最新值”。