五、内存模型和原子操作
5.1 C++中的标准原子类型
原子操作是不可分割的操作,它或者完全做好,或者完全没做。
标准原子类型的定义在头文件<atomic>
中,类模板std::atomic<T>
接受各种类型的模板实参,从而创建该类型对应的原子类型。
C++为内建类型定义了特例化的标准原子类型,并使用atomic_T
作为对T
类型特例化的别名:
std::atomic<bool>
→std::atomic_bool
std::atomic<char>
→std::atomic_char
对于一些内建的typedef类型,C++也提供了它们的特例化原子类型及别名,如:
std::atomic<std::size_t>
→std::atomic_size_t
标准原子类型是基于std::atomic<>
模板类定义的,该模板类支持初始化、值的存储与获取、比较交换操作等。std::atomic<bool>
完全基于该主模板类定义,所以std::atomic<bool>
支持的操作就是所有标准原子类型支持的操作,包含如下五种。
1. 初始化
原子对象的初始化是非原子的,不同初始化方法的执行流程为:
- 默认初始化。C++20前,只为静态变量和全局变量执行值初始化。C++20后,使用值初始化底层对象。
{ // 未初始化的原子对象 std::atomic_bool abool; }
- 使用内置类型初始化
std::atomic_bool abool1 = false; std::atomic<bool> abool2{true};
因为原子类型上的操作全是原子化的,在拷贝赋值的过程中,需要从源对象读取值,再写入目标对象。这是两个对象上的独立操作,其组合不可能是原子化的,所以原子对象禁止拷贝赋值和拷贝构造。
2. 值的存储与获取
有两种原子操作方法可以重新指定原子变量保存的值:
T operator=(T desired)
,如abool = true
。为了避免多个线程修改造成获取的返回结果不可预测,该赋值运算按值返回非原子类型的实参。void store(T desired)
,如abool.store(true)
同样有两种原子操作方法可以获取原子变量保存的值:
operator T()
,如(bool)abool
。通过重载的类型转换运算符以非原子类型方式获取保存的值T load()
,如abool.load()
。
可以原子性地执行读-修改-写操作,即获取原子变量的原值,并指定新值:
T exchange(T desired)
,如abool.exchange(true)
。返回非原子类型的原子变量底层值,并设置原子变量的值为desired
std::atomic<int> a = 3;
std::atomic<int> b{3};
std::cout << "[init] a: " << a.load() << "; b: " << (int)b << std::endl;
// [init] a: 3; b: 3
// 保存新的值
a = 5;
b.store(5);
std::cout << "[set] a: " << a.load() << "; b: " << (int)b << std::endl;
// [set] a: 5; b: 5
// 获取新的值
int old_a = a.exchange(7);
int old_b = b.exchange(7);
std::cout << "[exchange] old_a: " << old_a << "; old_b: " << old_b << std::endl;
// [exchange] old_a: 5; old_b: 5
std::cout << "[exchange] a: " << a.load() << "; b: " << (int)b << std::endl;
// [exchange] a: 7; b: 7
因为原子类型定义了类型转换运算符,所以当使用std::cout
可以直接输出std::atomic<int>
等类型的变量,这是因为隐式调用了类型转换运算符,实际输出的是普通类型的变量:
std::atomic<int> i = 0;
std::atomic<double> d = 1.2;
std::cout << "i: " << i << "; d: " << d << std::endl;
// i: 0; d: 1.2
// 实际隐式调用了 cout.operator<<((int)i)
3. 比较-交换(Compare and Swap, CAS)
比较交换操作是原子类型的编程基石,有两个成员函数可以完成:
bool compare_exchange_weak(T& expected, T desired)
bool compare_exchange_strong(T& expected, T desired)
它们原子地逐字节比较原子对象与**expected
**的值:
- 如果相等,将**
desired
保存到原子对象 **,返回true
- 如果不等,将原子对象的值保存到**
expected
**,返回false
compare_exchange_weak
可能会发生假性失败,即当*this = expected
时,也执行将原子对象的值保存到expected
并返回false
,但是性能更好。如果**compare_exchange_weak
用在循环**中,就可以通过多次循环避免假性失败的问题。(因为一些处理器不支持原子比较交换指令,使用循环+weak
会更高效)
compare_exchange_strong
不会发生假性失败,一定会严格的按照上述逻辑执行。如果对性能要求并不极致,通常使用该版本会更简单。
两者的区别可以参考文章——c++并发编程3. CAS原语
从以上逻辑可以看出,如果在while循环中执行以上两种函数,由于失败时会设置expected
等于原子对象的值,那么最多在第二次循环时一定会将desired
写入原子对象。即:
std::atomic<int> a_int = 5;
int exp = 6, des = 7;
int loop_num = 1;
while(!a_int.compare_exchange_weak(exp, des)) {
std::cout << "[" << loop_num << "] exp: " << exp << std::endl;
std::cout << "[" << loop_num << "] a_int: " << a_int.load() << std::endl;
}
std::cout << "[end] a_int: " << a_int.load() << std::endl;
/*
[1] exp: 5
[1] a_int: 5
[end] a_int: 7
*/
4. C++20的新操作
C++20起,还添加了以下操作:
void wait(T old)
:阻塞线程,每次调用notify_xxx
时去检查原子值,若值已经与old
不同,退出阻塞;否则继续阻塞。不调用notify_xxx
时,即使值改变了,也不会影响wait
。该函数可能会被虚假解锁,即没有任何线程调用notify_xxx
时,可能会去执行原子值与old
的对比而退出阻塞。void notify_one()
和void notify_all()
:如果有正在阻塞的线程,提醒一个或多个线程去检测原子值是否改变,判断是否可以退出阻塞。
std::atomic<int> a_int = 5;
void Worker() {
std::chrono::time_point beg = std::chrono::steady_clock::now();
a_int.wait(5);
std::chrono::time_point end = std::chrono::steady_clock::now();
std::cout << "wait time: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - beg)
<< std::endl;
}
int main(int argc, char const *argv[]) {
std::thread t(Worker);
// 多次调用 notify_all 提醒原子变量去检测原子值
std::this_thread::sleep_for(std::chrono::seconds(2));
a_int.notify_all();
std::this_thread::sleep_for(std::chrono::seconds(2));
a_int.store(6);
std::this_thread::sleep_for(std::chrono::seconds(2));
a_int.notify_all();
t.join();
return 0;
}
/*
wait time: 6001ms
*/
5. 无锁类型的判断
对于原子类型的原子操作,一般是由原子指令直接实现的,但是对于部分类型的原子操作,仅仅靠原子指令可能无法实现,这时原子操作会借助编译器和程序库的内部锁实现。为了判断某一原子类型的操作是基于什么实现的,C++提供了运行时判断方法和编译时判断方法。
运行时判断:几乎全部原子类型都包含成员函数t.is_lock_free()
,如果此类型对象上的所有原子操作都是无锁的返回true
,否则返回false
。
编译时判断:C++提供了多种方法在编译时判断原子类型是否无锁
- 使用宏
ATOMIC_xxx_LOCK_FREE
,该宏中的xxx
与要判断的标准原子类型相对应,其值包含三种:0
:原子类型一定有锁1
:部分机器上该类型无锁2
:原子类型一定无锁
- 使用静态成员常量
std::atomic<type>::is_always_lock_free
,原子类型始终为无锁则为true
,若它一定或有时为无锁则为false
- 使用非成员函数
std::atomic_is_lock_free( std::atomic<T>*)
,原子类型始终为无锁则为true
,若它决不或有时为无锁则为false
std::atomic_llong allong{2};
std::cout << "is_lock_free: " << allong.is_lock_free() << std::endl;
std::cout << "ATOMIC_LLONG_LOCK_FREE: " << ATOMIC_LLONG_LOCK_FREE << std::endl;
std::cout << "is_always_lock_free: " << allong.is_always_lock_free << std::endl;
std::cout << "atomic_is_lock_free: " << std::atomic_is_lock_free(&allong) << std::endl;
/**
is_lock_free: 1
ATOMIC_LLONG_LOCK_FREE: 2
is_always_lock_free: 1
atomic_is_lock_free: 1
*/
5.2 特定的原子类型及操作
1. std::atomic_flag
std::atomic_flag
并非是std::atomic<T>
的特例化,它是原子布尔类型,且保证一定是无锁的。该类型的对象有两种状态:
true
false
特点:
- 与
std::atomic<T>
类似,std::atomic_flag
也不支持拷贝构造和拷贝赋值 - 因为
std::atomic_flag
一定是无锁的,所以它也不提供is_lock_free()
成员函数
创建对象:
std::atomic_flag flag;
,默认初始化。C++20之前,其状态是未指定的;C++20起,状态被初始化为false
std::atomic_flag flag = ATOMIC_FLAG_INIT;
,对于C++20前,可以使用该宏将状态初始化为false
操作:
flag.clear()
:将状态原子地改为false
flag.test_and_set()
:将状态原子地改为true
,并返回之前的状态
C++20起,还添加了获取状态的函数和**与std::atomic<T>
**相同的阻塞唤醒函数:
flag.test()
:原子地返回值flag.wait(old)
、falg.notify_one()
、flag.notify_all()
自旋锁:在获取锁时不阻塞线程,通过循环不断尝试获取锁,直至成功。利用std::atomic_flag
可以简单完美的实现自旋锁。
class Spinlock {
public:
Spinlock() : flag_(ATOMIC_FLAG_INIT) {}
void Lock() {
while (flag_.test_and_set());
}
void UnLock() {
flag_.clear();
}
private:
std::atomic_flag flag_;
};
Spinlock sp;
int cnt = 0;
void AddCnt() {
for (int i = 0; i < 1000000; ++i) {
// 利用 atomic_flag 实现的自旋锁实现互斥
sp.Lock();
++cnt;
sp.UnLock();
}
}
- 自旋锁可以充分利用CPU,但是会造成性能的浪费。如果线程只是短时间阻塞,自旋锁非常高效。通常会将自旋锁与互斥量结合使用,需要阻塞等待时先在短时间内利用自旋锁等待;当超过一定时间后,转换为利用互斥量休眠等待。
std::atomic<bool>
是标准的bool原子类型,相比于std::atomic_flag
,它可以通过赋值和store
设置目标值,可以通过load
和类型转换运算符读取值,exchange
和std::atomic_flag::test_and_set()
也非常类似。std::atomic<bool>
相比于std::atomic_flag
更加的灵活,其缺点是并不保证所有实现都是无锁的。
2. 原子指针和原子整数
std::atomic<T*>
是原子化的指针,std::atomic<int>
、std::atomic<long>
等是原子化的整数,它们除了支持主模板定义的操作外,还特例化提供了原子化的算术运算操作。包含:
- 原子自增/自减运算符
operator++
、operator--
等 - 原子加减函数,返回加减前的值:
fetch_add(n)
:原子化的加n
fetch_sub(n)
:原子化的减n
- 原子复合赋值运算符,返回运算后的值(不是左侧对象的引用):
operator+=
operator-=
此外,原子整数还定义了原子位运算操作:
- 原子位运算函数,返回运算前的值:
fetch_and
、fetch_or
、fetch_xor
- 复合位运算,以值返回运算后的结果:
operator&=
、operator|=
、operator^=
std::atomic_int acnt{0};
int cnt{0};
std::vector<std::thread> pool;
for (int i = 0; i < 10; ++i) {
pool.push_back(std::thread([&]() {
for (int i = 0; i < 100000; ++i) {
++acnt;
++cnt;
}
}));
}
for (int i = 0; i < 10; ++i) {
pool[i].join();
}
std::cout << "acnt: " << acnt << std::endl;
std::cout << "cnt: " << cnt << std::endl;
/**
acnt: 1000000
cnt: 752130
*/
3. std::atomic<>
的泛化
可以给std::atomic<>
传入自定义类型作为模板实参,从而创建一个原子化的自定义类型。只有自定义类型是可平凡复制时,才能创建该类型的原子类型。
可平凡复制:可以将对象按字节复制到char
数组或其他对象中,并保有其原值。
对于原子化的自定义类型,支持主模板定义的初始化、值的存储与获取、比较交换操作等。
与所有的原子类型相同,即使自定义类型重载了比较运算符,原子化的自定义类型在执行比较交换操作时也是通过逐字节比较对象。
4. 原子化的智能指针
C++20开始,在头文件<memory>
中通过部分特例化std::atomic<>
定义了原子化的shared_ptr<T>
和weak_ptr<T>
:
std::atomic<std::shared_ptr<T>>
std::atomic<std::weak_ptr<T>>
它们支持主模板定义的各种初始化、值的存储与获取、比较交换操作等。
5. 非成员函数实现的原子操作
以上各种原子操作都以成员函数的方式使用,为了更广的兼容性,C++还定义了对应于这些成员函数操作的非成员函数。一般情况下,这些操作以atomic_
作为成员函数名的前缀,构成非成员函数版本的名字。
这些成员函数第一个参数是原子类型的指针,指向要操作的原子类型。
5.3 内存次序
在单线程中,编译器可能会对一段代码的执行顺序进行重排列,即代码的实际执行顺序可能与代码顺序不同。如下所示的代码:
int x = 0, y = 0;
{
++x;
++y;
}
- 实际执行时可能会先执行
++y
,后执行++x
C++的优化会保证同样的代码在单一线程下的执行一定会得到相同的结果。但是在多线程环境下可能会因为指令重排造成问题。比如,多线程环境下在上面的代码中,在没有对指令的重排进行限制时,当前线程首先执行了++y
,此时如果在另外一个线程中先获取y
的值,再获取x
的值,会出现y=1, x=0
的状态,这与代码的实际顺序不符。
为了保证代码可以按照我们想要的顺序执行,可以限制编译器及CPU对一个线程中指令的重新排列,从而避免因重排执行顺序导致代码执行结果与预期不符。如,限制++y
一定在++x
后执行,这样就保证了在多线程环境下也能获取到和代码顺序相符的结果。
对于原子类型的每种操作,都可以在所有参数后提供一个额外的std::memory_order
类型的内存序参数,用于限制编译器在当前线程中将该操作重排到某些位置。
注意:所有的内存序都只作用于指定内存序的操作所运行的线程,不会跨线程交换指令执行的顺序,之所以能够利用内存序保证多个线程中原子操作的执行顺序,是因为利用了如while
循环等的手段,等待另一个线程中的某个操作A执行完成,如果在那个线程中限制了A操作前的指令不能重排到A后,那么在当前线程while
循环结束后表示A执行完成,则当前线程一定能正确获得A之前指令的操作结果。
std::memory_order
包含六种,按照对指令重排的限制程度从强到弱可以分为四类:
- 顺序一致次序(Sequentially-consistent ordering):
std::memory_order_seq_cst
- 释放-获取次序(Release-Acquire ordering)
std::memory_order_release
std::memory_order_acquire
std::memory_order_acq_rel
- 释放-消费次序(Release-Consume ordering)
std::memory_order_consume
- 宽松次序(Relaxed ordering)
std::memory_order_relaxed
1. 顺序一致次序
顺序一致次序是原子操作的默认内存序。即,当调用某个原子操作,且没有指定任何内存序参数时,默认使用的就是std::memory_order_seq_cst
。
顺序一致性规定:
- 同一线程内的多个顺序一致的操作会按照代码顺序执行
- 一个顺序一致的操作结束后,操作结果会对后续代码和其他线程立即可见(操作执行结束后,会从缓存立即同步到所有使用它的地方,避免像
relaxed
序一样可能会延迟同步结果)
顺序一致次序是最强的约束,它可以保证程序的运行严格按照代码顺序执行,但是在部分机器上为了实现该约束可能会造成较大的性能损失。
// 虽然原子操作内存序的默认参数就是 std::memory_order_seq_cst,
// 但是以下代码仍然显式指定了 std::memory_order_seq_cst 以突出显式
#include <atomic>
#include <cassert>
#include <iostream>
#include <thread>
/** @brief 先修改 x,再修改 y */
void WriteXAndY() {
x.store(true, std::memory_order_seq_cst);
y.store(true, std::memory_order_seq_cst);
}
/** @brief 先读取 x,再读取 y */
void ReadXThenY() {
while (!x.load(std::memory_order_seq_cst));
if (y.load(std::memory_order_seq_cst)) {
std::cout << "111" << std::endl;
++z;
}
}
/** @brief 先读取 y,再读取 x */
void ReadYThenX() {
while (!y.load(std::memory_order_seq_cst));
if (x.load(std::memory_order_seq_cst)) {
++z;
std::cout << "222" << std::endl;
}
}
std::atomic<bool> x, y;
std::atomic<int> z;
int main() {
x = false;
y = false;
z = 0;
std::thread c(ReadXThenY);
std::thread d(ReadYThenX);
std::thread e(WriteXAndY);
e.join();
c.join();
d.join();
return 0;
}
- 以上代码中,无论编译器或CPU对指令怎么重排,按照内存序的约束,
ReadYThenX
一定会执行。即,当y
被修改为true
时,x
一定也已经被修改为true
了。但是ReadXThenY
不一定执行,因为按照代码顺序,执行完x.store(true, std::memory_order_seq_cst)
后,可能会发生进程的调度,造成不能进入ReadXThenY
的if
判断内。
2. 宽松次序
宽松次序规定在一个线程内:
- 对同一原子变量的操作,按照代码顺序执行,操作结果对其他线程可能会延迟可见(最终一定会可见)
- 对不同原子变量的操作可以重排顺序
宽松次序是最松的约束,指定宽松次序后,允许编译器根据需要重排指令的顺序以代码提高运行效率。
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<int> x(0), y(1);
int r1 = 2, r2 = 3;
void WriteX() {
r1 = y.load(std::memory_order_relaxed);
x.store(r1, std::memory_order_relaxed);
}
void WriteY() {
r2 = x.load(std::memory_order_relaxed);
y.store(42, std::memory_order_relaxed);
}
int main(int argc, char const *argv[]) {
std::thread a(WriteX);
std::thread b(WriteY);
a.join();
b.join();
return 0;
}
- 以上原子操作都指定了
std::memory_order_relaxed
,编译器如果对执行指令重排,再叠加进程调度,最终的执行结果可能会出现r1 = 42, r2 = 42
的情况。即重排后先执行y.store
(指令重排)、再执行r1 = y.load
(进程调度)、再执行x.store(r1)
、最后执行r2 = x.load
(进程调度+指令重排)
宽松次序最常见的应用是计数器自增:
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
std::atomic<int> cnt(0);
/** @brief 对 cnt 递增 100 次 */
void Work() {
for (int i = 0; i < 100; ++i) {
cnt.fetch_add(1, std::memory_order_relaxed);
}
}
int main() {
std::vector<std::thread> ts;
for (int i = 0; i < 10; ++i) {
ts.emplace_back(Work);
}
for (int i = 0; i < 10; ++i) {
ts[i].join();
}
std::cout << "cnt: " << cnt.load() << std::endl;
}
/*
cnt: 1000
*/
- 因为计数器自增只要求原子性,对执行的顺序并不敏感,且不会影响与其他线程的同步,即使编译器为了优化性能发生了指令的重排,对最终的结果也不会造成影响
3. 释放-获取次序
释放获取次序主要包含两个,它们都是只影响操作所在的线程:
std::memory_order_release
:用来修饰一个写操作(如store
),限制在该写操作前的所有操作(包含非原子、原子及宽松原子的读写)不能重排到该写操作之后;且如果有操作发生了内存写入,写入的结果会在运行完该写操作后,在其他线程立即可见std::memory_order_acquire
:用来修饰一个读操作(如load
),限制在该读操作后的所有操作(包含非原子、原子及宽松原子的读写)不能重排到该读操作前
此外,还有一个同时作用获取释放的次序:
std::memory_order_acq_rel
:修饰一个读-改-写操作(如exchange
),同时包含上面两个修饰符的作用
从定义可以看出,写操作的释放保证一个线程中该写操作之前的代码一定执行完成,读操作的获取保证一个线程中该读操作后的所有代码都在读操作后执行。因此,可以利用该特性,实现内存操作的同步。
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<bool> x = false, y = false;
int data = 0;
void Producer() {
x.store(true, std::memory_order_relaxed);
data = 42;
// memory_order_release 声明之前的所有代码不能重排到后面
y.store(true, std::memory_order_release);
}
void Consumer() {
while (!y.load(std::memory_order_acquire));
// memory_order_acquire 声明之后的代码不能重排到前面
if (x.load() == true && data == 42) {
std::cout << "Yep\n";
}
}
int main(int argc, char const *argv[]) {
std::thread a(Producer);
std::thread b(Consumer);
a.join();
b.join();
return 0;
}
- 在
Consumer
中由于循环等待y.load
返回true
,当退出while
循环时,说明Producer
中y.store(true, std::memory_order_release)
一定执行完成,因为有参数memory_order_release
,所以y.store
前的所有代码也一定执行完成(即使x.store
指定了memory_order_relaxed
)。此时,在Consumer
的while
循环后的代码一定能够看到Producer
的y.store
前的修改,所以会输出Yep
实际上,顺序一致次序等价于同时指定了释放-获取次序,即:
- 对写操作的
std::memory_order_seq_cst
,就是执行std::memory_order_release
内存序 - 对读操作的
std::memory_order_seq_cst
,就是执行std::memory_order_acquire
内存序
4. 释放-消费操作
释放-消费操作与释放获取操作类似,区别在于使用时将std::memory_order_acquire
替换为std::memory_order_consume
:
std::memory_order_consume
:作用于某个原子变量的读操作(如load
),限制当前线程中该原子变量及依赖该原子变量的所有操作不能重排到该读操作之前
代码:
std::atomic<int *> global_addr{nullptr};
void Func(int *data) {
int *addr = global_addr.load(std::memory_order_consume);
int d = *data;
int f = *(data + 1);
if (addr) {
int x = *addr;
}
}
- 由于
global_addr
的读操作指定了std::memory_order_consume
,所以依赖于global_addr
的addr
和x
的相关操作不能重排到global_addr.load
前。d
和f
的相关代码不依赖global_addr
,所以编译器可以将这些代码重排到合适的位置。
将std::memory_order_release
和std::memory_order_consume
结合,可以实现不同线程中只同步相互依赖的变量。如:
#include <atomic>
#include <iostream>
#include <thread>
#include <string>
std::atomic<bool> abool = false;
int data;
void Producer() {
data = 42;
abool.store(true, std::memory_order_release);
// 保证 abool.store 之前的所有操作都执行完成
}
void Consumer() {
// 只保证与 abool 相关的操作不会重排到前面
while (!abool.load(std::memory_order_consume));
if (abool.load() == true) {
std::cout << "abool: Yep\n";
}
// data 可以被重排到 while 前,可能会输出 date: No
if (data == 42) {
std::cout << "data: Yep\n";
} else {
std::cout << "date: No\n";
}
}
int main(int argc, char const *argv[]) {
std::thread a(Producer);
std::thread b(Consumer);
a.join();
b.join();
return 0;
}
- 只有
abool
的值会在两个线程间同步,在Consumer
中如果重排data
的判断语句到abool.load
前,会输出date: No
5.4 栅栏
以上的内存序都局限于某个原子操作,C++11还定义了可以独立于原子操作使用的内存栅栏,相比于原子操作的内存序,使用栅栏可以强制施加内存次序,实现更强的同步效果。
内存栅栏主要通过全局函数设置:
void atomic_thread_fence(std::memory_order order)
当线程运行到栅栏函数处时,栅栏会对线程中其他原子操作的重排施加限制,从而使得这些操作满足特定的执行顺序。
1. 内存栅栏的逻辑
当六种不同的内存序做参数时,可以将栅栏类型分为三种:
release fence
:用于阻止当前线程中**fence
前的内存操作重排到fence
后的任意store
之后 **(store
可以是任意内存序)。原子操作的release
只限制了之前的操作不能重排到release
后;release fence
则限制不能排到release
后的任意store
后(添加了store
条件),包括:std::atomic_thread_fence(std::memory_order_release)
根据以上原则,以下两种代码有相同的效果:
// 代码 1 std::string* p = new std::string("Hello"); ptr.store(p, std::memory_order_release); // 代码 2 std::string* p = new std::string("Hello"); std::atomic_thread_fence(memory_order_release); // 由于release fence的限制,即使ptr.store是releaxed的,也会在p的初始化后执行 ptr.store(p, std::memory_order_relaxed);
acquire fence
:用于阻止当前线程中**fence
**后的内存操作重排到**fence
前的任意load
之前**(load
可以是任意内存序)。原子操作的acquire
只限制了之后的操作不能重排到acquire
前;acquire fence
则限制不能排到release
前的任意load
前(添加了load
条件),包括:std::atomic_thread_fence(std::memory_order_acquire)
std::atomic_thread_fence(std::memory_order_consume)
根据以上原则,以下两种代码有相同的效果:
// 代码 1 std::string* p; while(p = ptr.load(std::memory_order_acquire)); assert(*p == "hello") // 代码 2 std::string* p; // 由于acquire fence的限制,即使ptr.load是relaxed的,也会在*p前执行 while(p = ptr.load(std::memory_order_relaxed)); std::atomic_thread_fence(memory_order_acquire); assert(*p == "hello")
full fence
:release fence
和acquire fence
的组合,可以同时实现以上两者的功能std::atomic_thread_fence(std::memory_order_acq_rel)
:release fence
和acquire fence
的组合std::atomic_thread_fence(std::memory_order_seq_cst)
:额外保证有完全顺序一致性的full fence
,具有最强的约束
std::atomic_thread_fence(std::memory_order_relaxed)
类型的栅栏不限制任何重排
2. 内存栅栏的使用
根据以上栅栏内存序,通过和原子内存次序的组合,可以形成三种同步。
1. release fence
-acquire atomic
同步
如下在两个线程运行的代码:
std::atomic<bool> atm;
// Thread A
void WorkA() {
// ops1
std::atomic_thread_fence(std::memory_order_release);
atm.store(true, std::memory_order_relaxed);
}
// Thread B
void WorkB() {
while (!atm.load(std::memory_order_acquire));
// ops2
}
可以构成如下执行流程:
Thread A | Thread B |
---|---|
ops1 | |
F:release fence | Y:while(!atm.load(acquire)) |
X:atm.store( any_order ) | ops2 |
- 由于
release fence
操作限制ops1
重排到X后面,atomic acquire
限制ops2
重排到Y前面,所以当Y执行完毕时,表示F前的所有操作一定执行完成了。因此该release fence
与acquire atomic
使得两个线程中的ops1
部分的代码一定早于ops2
部分的代码执行 - 根据上面的规则,X的操作只要是
store
操作即可,内存序可以任意
2. release atomic
-acquire fence
同步
如下在两个线程运行的代码:
std::atomic<bool> atm;
// Thread A
void WorkA() {
// ops1
atm.store(true, std::memory_order_release);
}
// Thread B
void WorkB() {
while (!atm.load(std::memory_order_relaxed));
std::atomic_thread_fence(std::memory_order_acquire);
// ops2
}
可以构成如下执行流程:
Thread A | Thread B |
---|---|
ops1 | Y:while(!atm.load( any_order )) |
X:atm.store(release) | F:acquire fence |
ops2 |
- 由于
acquire fence
限制ops2
重排到Y之前,atomic release
限制ops1
重排到X后面,当Y执行完毕时,X一定执行完成。因此,release atomic
与acquire fence
使得ops1
的代码一定比ops2
的代码先执行 - 根据上面的规则,Y的操作只要是
load
操作即可,内存序可以任意
3. release fence
-acquire fence
同步
如下两个线程运行的代码:
std::atomic<bool> atm;
// Thread A
void WorkA() {
// ops1
std::atomic_thread_fence(std::memory_order_release);
atm.store(true, std::memory_order_relaxed);
}
// Thread B
void WorkB() {
while (!atm.load(std::memory_order_relaxed));
std::atomic_thread_fence(std::memory_order_acquire);
// ops2
}
可以构成如下执行流程:
Thread A | Thread B |
---|---|
ops1 | Y:while(!``var.load( any_order )``) |
FA:release fence | FB:acquire fence |
X:var.store( any_order ) | ops2 |
- 由于
release fence
限制ops1
必须在X之前执行,acquire fence
限制ops2
必须在Y之后执行。当Y执行完成后,表示X一定执行过了。因此,release fence
与acquire fence
使得ops1
的代码一定早于ops2
执行