5.1内存模型基础
基本类型与内存占用
- 基础数据类型如int或char,无论其值大小如何,都只会占用一个内存位置。这意味着即使它们是数组的一部分,也会单独占用一个内存位置
对象的内存表示
- 每个变量都是对象,都在内存中有对应的位置。无论是简单变量还是复杂数据结构,都会在内存中有自己的空间位置
并发编程中的内存访问
-
如果多个线程操作同一内存位置,可能会引发问题,尤其当涉及数据修改时
-
每个变量都是一个对象,包括成员变量
-
每个对象至少占用一个内存位置
-
基本类型变量,如 int 或 char,恰好占用一个内存位置,无论它们的大小如何,即使它们是相邻的或是一个数组的一部分
-
相邻的位字段是同一个内存位置的一部分
struct my_data
{
int i;
double d;
unsigned bf1 : 10;
int bf2 : 25;
int /*bf3*/ : 0;
int bf4 : 9;
int i2;
char c1, c2;
std::string s;
};
bf3 通常没有自己的内存位置,而 bf4 会有自己的内存位置

5.1.2对象、内存位置和并发
线程安全与内存位置
当多个线程访问不同的内存位置时,它们之间不会互相干扰,保持各自独立执行任务。这种情况下,系统线程安全,无需额外的同步措施
只读共享数据
如果多个线程同时访问相同的内存位置,但都以只读方式操作,那么也不会出现线程安全问题。只读共享数据不需要特别的保护机制
修改共享数据的风险
当多个线程对同一内存位置进行写操作时,就会产生线程安全问题。必须采取适当的同步策略来避免数据不一致或竞态条件的发生
原子操作在避免未定义行为中的作用
使用原子操作访问内存位置是避免未定义行为的有效方法之一。虽然它不能完全阻止数据竞争,但可以确保程序在遇到并发问题时仍保持定义行为,提高程序的可靠性
5.1.3修改顺序
对象修改顺序的一致性是通过程序中的同步机制和内存模型的规则在运行时实现的
修改顺序的构成
- 对象修改顺序是由程序中所有线程对该对象的写入操作组成的,从对象的初始化开始
线程间对修改顺序的共识
- 在任何给定的程序执行中,系统中的所有线程必须就修改顺序达成一致
原子操作与修改顺序的关系
- 如果使用原子操作,编译器会负责确保必要的同步,从而维护修改顺序
尽管所有线程必须对每个对象的修改顺序达成一致,但它们不必对不同对象上的操作顺序达成一致
5.2 C++中的原子操作和类型
原子操作的不可分割性:原子操作是系统中无法被其他线程观察到中间状态的操作,要么完成,要么未完成
5.2.1标准原子类型
如何确定原子类型是否为lock-free
- 通过调用is_lock_free()成员函数,用户可以确定原子类型是否直接使用原子指令执行
内部锁的使用与性能影响
- 如果原子操作本身使用内部锁,可能无法实现预期的性能提升
C++17后的标准原子类型特性
- C++17以后,所有原子类型都有一个静态常量成员变量X::is_always_lock_free,表示该原子类型在所有支持的硬件上是否总是lock-free
可以使用互斥锁来使作表现得像原子操作。标准的原子类型本身可能就是通过这种方式进行模拟的
ATOMIC_BOOL_LOCK_FREE、ATOMIC_CHAR_LOCK_FREE等宏,指定了对于指定的内置类型及其无符号对应类型的相应原子类型的无锁(lock-free)状态
- 0:从来不是无锁的
- 1:是一个运行时属性
- 2:总是无锁的
std::atomic_flag
- std::atomic_flag是唯一没有提供is_lock_free()成员函数的原子类型。它是一个简单的布尔标志,其操作必须为无锁,这是实现其他原子类型的基础。
其他原子类型
- 除了std::atomic_flag外,其余的原子类型都是通过std::atomic类模板的特化来访问的,它们功能更全面,但可能不是无锁的
平台差异
- 在大多数流行平台上,预期所有内置类型的原子变体(如std::atomic)都是无锁的,但这并不是必须的
std::atomic_flag 类型的对象被初始化为clear状态,然后可以被查询和设置(使用 test_and_set() 成员函数)或者被清除(使用 clear() 成员函数)。
- 除了直接使用 std::atomic<> 类模板,还可以使用下表中的原子类型
- 原子类型 对应的特化
atomic_bool std::atomic<bool>
atomic_char std::atomic<char>
atomic_schar std::atomic<signed char>
atomic_uchar std::atomic<unsigned char>
atomic_int std::atomic<int>
atomic_uint std::atomic<unsigned>
atomic_short std::atomic<short>
atomic_ushort std::atomic<unsigned short>
atomic_long std::atomic<long>
atomic_ulong std::atomic<unsigned long>
atomic_llong std::atomic<long long>
atomic_ullong std::atomic<unsigned long long>
atomic_char16_t std::atomic<char16_t>
atomic_char32_t std::atomic<char32_t>
atomic_wchar_t std::atomic<wchar_t>
除了基本的原子类型,C++ 标准库还提供了一组对应标准库typedef类型定义的 typedef。通常,使用 std::atomic 会更加简单,而不是使用替代名称
成员函数的作用
- 标准原子类型提供了一些成员函数,如load()和store(),可以直接加载和存储原子变量的值,以及exchange()、compare_exchange_weak()和compare_exchange_strong()等函数,用于实现原子操作的同步和比较
赋值运算符的使用
- 标准原子类型支持适当的复合赋值运算符,如+=、-=、*=等,用于实现原子变量的赋值操作。并有对应的成员函数fetch_add(), fetch_or()等
返回值的处理方式
- 赋值运算符和成员函数的返回值可以是存储的值(对于赋值运算符)或操作前的值(对于命名函数),避免了通常的引用返回方式可能引发的竞态条件问题
为了从引用中获取存储的值,代码将不得不执行一个单独的读取操作,这允许另一个线程在赋值和读取之间修改该值,从而为数据竞争打开了大门
std::atomic模板可以用于创建自定义类型的原子变量
原子操作的种类和限制
- std::atomic的操作主要包括load(), store()等,每种操作都有其特定的内存排序选项
内存排序选项的作用和影响
- 内存排序选项决定了原子操作的内存顺序语义,不同的操作类别有不同的允许值
如果没有指定内存排序参数,std::atomic将使用最严格的std::memory_order_seq_cst(sequentially consistent,按顺序一致)作为默认排序行为,以确保数据的原子性和可见性
内存排序选项被分为三类:
- 存储操作:可以具有 memory_order_relaxed、memory_order_release 或 memory_order_seq_cst 顺序;
- 加载操作:可以具有 memory_order_relaxed、memory_order_consume、memory_order_acquire 或 memory_order_seq_cst 顺序;
- 读修改写操作:可以具有 memory_order_relaxed、memory_order_consume、memory_order_acquire、memory_order_release、memory_order_acq_rel 或 memory_order_seq_cst 顺序。
5.2.2对std::atomic_flag的操作
-
std::atomic_flag 对象可以处于两种状态之一:设置(set:true)或清除(clear:false)。被故意设计得非常基础,仅作为构建块使用
-
std::atomic_flag 对象初始化后,只能执行析构函数、clear() 和 test_and_set() 成员函数
std::atomic_flag 类型的对象必须使用 ATOMIC_FLAG_INIT 进行初始化。这将标志初始化为清除状态:
std::atomic_flag f = ATOMIC_FLAG_INIT;
它是唯一需要这种特殊初始化处理的原子类型,也是有保证总是无锁(lock-free)的唯一类型
clear() 函数是存储操作(涉及到对原子对象状态的改变),不能有 memory_order_acquire 或 memory_order_acq_rel 语义,但 test_and_set() 是一个读修改写操作,因此可以应用任何内存排序。与每个原子操作一样,默认情况下两者都是 memory_order_seq_cst。
f.clear(std::memory_order_release);
bool x = f.test_and_set(); //默认内存排序
clear() 函数使用 memory_order_release 内存排序进行调用,这确保了在清除标志之前的所有存储操作对其他线程可见。test_and_set() 函数尝试将标志设置为设置状态,并返回设置之前的值。
原子类型的所有操作都被定义为原子的,拷贝构造或赋值必须首先从一个对象读取值,然后将该值写入另一个对象,它们的组合不可能是原子的。
std::atomic_flag 的功能集有限,非常适合用作自旋锁互斥锁(spinlock mutex)。最初,标志位是清除的,互斥锁是解锁的。要锁定互斥锁,循环调用 test_and_set() 直到旧值为假,表示该线程将值设置为真。解锁互斥锁只需清除标志位:
class spinlock_mutex{ // 忙等待锁
private:
std::atomic_flag flag; // 原子标志用于表示锁的状态
public:
// 构造函数初始化标志位为未设置(clear)状态,表示锁未被占用
spinlock_mutex() : flag(ATOMIC_FLAG_INIT) {} // false
void lock(){// 尝试获取锁,如果锁已被占用,则忙等待(spin-wait)直到锁可用
while (flag.test_and_set(std::memory_order_acquire)); // 如果成功设置(旧值false),表示锁成功
}
void unlock(){// 释放锁,将标志位清除,表示锁现在可用
flag.clear(std::memory_order_release);
}
};
- 返回 true 表示锁已被其他线程占用,当前线程需要继续循环尝试
- std::memory_order_release 确保在特定操作之前的所有内存写入操作对于后续使用 std::memory_order_acquire、std::memory_order_consume 或 std::memory_order_seq_cst 内存序秩序进行的原子读取操作可见
5.2.3对std::atomic的操作
std::atomic是一个比 std::atomic_flag 更具功能的布尔标志。尽管仍然不能进行拷贝构造或拷贝赋值,但可以从一个非原子的 bool 类型构造它,或进行赋值
std::atomic<bool> b(true);
b = false;
从非原子 bool 类型的赋值,返回一个赋值后的布尔值。这是原子类型的另一个常见模式:它们支持的赋值操作符返回值而不是引用
std::atomic<bool> flag;
bool condition = true;// 使用赋值操作符设置flag的值,并获取设置后的值
bool result = flag = condition;// 现在,result变量包含了设置后的flag的值
如果返回对原子变量的引用,那么任何依赖于赋值结果的代码将不得不显式加载值,可能会得到另一个线程修改后的结果
与 std::atomic_flag 使用功能有限的 clear() 函数不同,写入操作(true 、false)是通过调用 store() 来完成的。同样,test_and_set() 被更通用的 exchange() 成员函数取代,它允许用选择的新值替换存储的值,并原子地检索原始值
std::atomic<bool> b;
bool x = b.load(std::memory_order_acquire);
b.store(true); // 存储值true到原子变量b
x = b.exchange(false, std::memory_order_acq_rel); // 将b的值交换为false,并返回原始值
- store() 是一个存储操作, load() 是一个加载操作。exchange() 是一个读修改写操作
- exchange() 并不是 std::atomic 支持的唯一读修改写操作
根据当前值存储新值(或不存储)
这个新操作被称为比较交换(compare-exchange),它以 compare_exchange_weak() 和 compare_exchange_strong() 成员函数的形式存在。
它将原子变量的值与提供的期望值进行比较:
- 相等,则存储提供的期望值
- 不相等,则将预期值更新为原子变量的值
比较交换函数返回一个布尔值,如果执行了存储操作则为真(因为值相等),否则为假。
对于 compare_exchange_weak(),即使原始值等于期望值,存储操作也可能不成功
- 这种情况最有可能发生在缺乏单一比较和交换指令的机器上,可能是因为执行操作的线程在必要的指令序列中间被切换出去,而操作系统在其位置安排了另一个线程
compare_exchange_weak() 可能会伪失败,它通常必须在循环中使用:
bool expected = false;
extern atomic<bool> b; // 在其他地方设置,可能被多个线程修改
while (!b.compare_exchange_weak(expected, true) && !expected);
-
只要 expected 仍然是 false,表示 compare_exchange_weak() 调用发生伪失败,需要继续循环。
-
第二个参数true:要存储到原子变量 b 中的新值
-
如果 compare_exchange_weak() 失败,expected 被更新为 b 的当前值
compare_exchange_strong() 保证只有在值不相等的情况下才返回 false。这可以消除像上面展示的循环的需要
- 如果存储值的计算很简单,使用 compare_exchange_weak() 可能是有益的,以避免在 compare_exchange_weak() 可能失败的平台上进行双重循环(compare_exchange_strong() 内部实现包含一个循环)。
- 如果存储值很耗时,使用 compare_exchange_strong() 可能是有意义的,以避免在期望值没有变化时重新计算存储值
比较交换函数允许在成功和失败的情况下内存排序语义有所不同
- 成功:可能希望具有 memory_order_acq_rel 语义
- 失败:则具有 memory_order_relaxed 语义
- 失败的比较交换不会执行存储操作,因此它不能具有 memory_order_release 或 memory_order_acq_rel 语义
- 也不能为失败提供比成功更严格的内存排序
如果没有为失败指定排序,它被假设与成功时相同,只是释放部分被剥离:
- memory_order_release 变为 memory_order_relaxed
- memory_order_acq_rel 变为 memory_order_acquire。
- 如果两者都没有指定,默认为 memory_order_seq_cst
以下两个对 compare_exchange_weak() 的调用是等效的:
std::atomic<bool> b;
bool expected;
b.compare_exchange_weak(expected, true,memory_order_acq_rel, memory_order_acquire);
b.compare_exchange_weak(expected, true,memory_order_acq_rel);
5.2.4对std::atomic<T*>:指针运算的操作
std::atomic<T*>与std::atomic 接口相同,尽管它是在相应的指针类型值上而不是布尔值上操作
std::atomic的store()函数
- std::atomic的store()函数用于设置新的指针值,接受一个T*类型的参数作为新值。
std::atomic的load()函数
- std::atomic的load()函数用于获取当前存储的指针值,返回类型为T*。
std::atomic的exchange()函数
- std::atomic的exchange()函数用于交换当前的指针值与给定的新值,返回旧的指针值。
-
std::atomic<T*> 提供的新操作是指针算术操作。基本操作由 fetch_add() 和 fetch_sub() 成员函数提供,它们对存储的地址进行原子加法和减法, += 和 -= 运算符,以及递增和递减
-
fetch_add() 和 fetch_sub() 返回原始值(所以 x.fetch_add(3) 会更新 x 以指向第四个值,但返回指向数组中第一个值的指针)。这个操作也称为exchange-and-add,是原子读修改写操作
class Foo{};
Foo some_array[5];
std::atomic<Foo*> p(some_array);
Foo* x = p.fetch_add(2);
assert(x == some_array);
assert(p.load() == &some_array[2]);
x = (p -= 1);
assert(x == &some_array[1]);
assert(p.load() == &some_array[1]);
- fetch_add() 用于将指针 p 增加 2,并返回旧值(指向数组的第一个元素)
- p 将指针减少 1,并返回新值(指向数组的第二个元素)。这些指针算术操作是原子的,确保在多线程环境中指针的更新是安全的
函数形式还允许将内存顺序语义指定为额外的函数调用参数:
p.fetch_add(3, std::memory_order_release);
- 对于运算符形式来说,指定排序语义是不可能的,因为没有提供信息的方法:因此这些形式总是具有 memory_order_seq_cst 语义
其余的基本原子类型都是一样的:它们都是原子整数类型,并且彼此具有相同的接口
5.2.5标准原子整型的操作
原子整型的复合赋值操作
- 原子整型支持fetch_add(), fetch_sub(), fetch_and(), fetch_or(), fetch_xor()等操作,以及这些操作的复合赋值形式(+=, -=, &=, |=, ^=),命名函数原子地执行其操作并返回旧值,而复合赋值运算符返回新值
原子整型操作集
- std::atomic等原子整型类型除了常规的加载、存储、交换等操作外,还提供了一系列的复合赋值操作
原子整型的增量和减量操作
- 对于原子整型,我们还可以执行前缀和后缀的增量和减量操作,如++x, x++, --x, x–
没有除法、乘法和位移运算符
5.2.6 std::atomic<>类模板
允许创建自定义类型的原子类型
-
为了对自定义类型 (UDT) 使用 std::atomic<>,这个类型必须有一个微不足道的拷贝赋值运算符。因此类型不能有虚函数或虚基类,且必须使用编译器生成的拷贝赋值运算符,自定义类型的每个基类和非静态数据成员也必须有一个微不足道的拷贝赋值运算符。这允许编译器使用 memcpy() 或等效操作进行赋值操作
-
std::atomic 提供与 std::atomic 相同的接口
-
比较交换操作会进行位比较。如果类型提供了具有不同语义的比较操作,或者类型有不参与正常比较的填充位,那么即使值比较相等,这也可能导致比较交换操作失败
- 数据竞争:编译器它将为所有操作使用一个内部锁。如果允许使用用户提供的拷贝赋值或比较运算符,这可能将受保护数据的引用作为参数传递给用户提供的函数(违背了第 3 章的指导方针)
- 死锁:如果用户的自定义操作涉及到等待其他锁或资源,而当前线程已经持有一个锁,这可能导致死锁的情况
- 性能问题:这些限制增加了编译器能够直接为 std::atomic 使用原子指令(并使特定实例无锁)的机会。因为它可以将自定义类型视为一组原始字节
如果使用 std::atomic<> 与自定义类型一起使用,并且该类型定义了一个相等比较运算符,而该运算符与使用 memcmp 进行比较不同;因为尽管值在其他方面相等,但它们的表示形式不同,操作可能会失败。(注:浮点类型,没有提供标准的原子类型,也适用该规则)
#include <atomic>
#include <iostream>
struct MyClass {
float value;
int flags;
MyClass(float v = 0.0f, int f = 0) : value(v), flags(f) {}
bool operator==(const MyClass& other) const {
return value == other.value; // 忽略 flags
}
};
int main() {
std::atomic<MyClass> atomicValue;
atomicValue.store(MyClass(1.0f, 0)); // 假设这是从某处获取的旧值
MyClass oldVal(MyClass(1.0f, 1)); // 旧值,flags 为 0
MyClass newVal(MyClass(1.0f, 1)); // 新值,flags 为 1
bool success = atomicValue.compare_exchange_strong(oldVal, newVal);
if (!success) {
std::cout << "compare_exchange_strong 操作失败。" << std::endl;
std::cout << "存储的值和旧的值在value上相等,但在表示形式上不相等。" << std::endl;
}
else {std::cout << "compare_exchange_strong 操作成功。" << std::endl; }
}
- 如果自户定义类型(UDT)的大小与 int 或 void* 相同(或更小),大多数常见平台将能够为 std::atomic 使用原子指令
- 一些平台还能够为用户定义类型使用原子指令,这些类型的大小是 int 或 void* 的两倍。这些平台通常支持所谓的双字比较和交换(DWCAS)指令,与 compare_exchange_xxx 函数相对应
- 数据结构越复杂,就越可能希望对其进行不仅仅是简单赋值和比较的操作。最好使用 std::mutex 来确保数据适用于所需的操作

5.2.7用于原子操作的非成员函数
- 多数情况下,非成员函数以相应的成员函数命名(将指向原子对象的指针作为第一个参数),但加上了 atomic_ 前缀(如 std::atomic_load())。在有机会指定内存排序时,有两种变体:
- std::atomic_store(&atomic_var,new_value)
- std::atomic_store_explicit(&atomic_var,new_value,std::memory_order_release))
-
为了 C 兼容,所有情况下都使用指针而不是引用(成员函数可能用引用)
-
std::atomic_compare_exchange_weak_explicit() 还要求指定成功和失败的内存顺序,而比较交换成员函数既有一个单一的内存顺序形式(默认为 std::memory_order_seq_cst),也有一个分别接受成功和失败内存顺序的重载
int main() {
std::atomic<int> atomicVar(0); // 初始值为 0
int expected = 0; // 预期值为 0
int newValue = 1; // 我们想要设置的新值为 1
bool success;
// 使用 compare_exchange_weak_explicit 尝试更新 atomicVar 的值
// 成功时使用 memory_order_release 内存顺序,失败时使用 memory_order_relaxed
success = std::atomic_compare_exchange_weak_explicit(
&atomicVar,&expected,newValue,std::memory_order_release,std::memory_order_relaxed); if (success) {std::cout << "atomicVar的值已经成功更新为 " << newValue << std::endl;
} else {std::cout << "compare_exchange_weak操作失败。" << std::endl;}
}
std::atomic_flag 上的操作在名称中明确了 flag 部分:std::atomic_flag_test_and_set(),std::atomic_flag_clear()。指定内存排序的额外变体带有 _explicit 后缀:std::atomic_flag_test_and_set_explicit() 和 std::atomic_flag_clear_explicit()。
C++ 标准库还提供了std::shared_ptr<> 实例的原子访问 。这打破了只有原子类型才支持原子操作的原则。但是,C++ 标准委员会认为提供这些额外的函数足够重要
std::shared_ptr<my_data> p;
void process_global_data(){
std::shared_ptr<my_data> local = std::atomic_load(&p);
process_data(local);
}
void update_global_data(){
std::shared_ptr<my_data> local(new my_data);
std::atomic_store(&p, local);
}
并发TS还提供了 std::experimental::atomic_shared_ptr类型。它被提供为一种单独的类型,因为这样可以允许无锁实现,不会对普通的 std::shared_ptr 实例强加额外的成本。但仍然需要检查它在平台上是否无锁,可以通过 is_lock_free 成员函数来测试
5.3 同步操作和强制顺序
假设有两个线程,其中一个线程正在填充一个数据结构,以便第二个线程读取。第一个线程设置一个标志来表示数据已经准备好,第二个线程在标志被设置之前不会读取数据
std::vector<int> data;
std::atomic<bool> data_ready(false);
void reader_thread(){
while (!data_ready.load()){
std::cout << "data not ready,waiting..." << "\n";
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
std::cout << “The answer = ” << data[0] << “\n”; // happens after(非原子)
}
void writer_thread(){
data.push_back(42); // happens before(非原子)
data_ready = true; // 原子的
}
int main(){
std::thread reader(reader_thread);
std::thread writer(writer_thread);
reader.join();
writer.join();
}
-
如果没有强制的顺序,进行非原子读写访问相同数据是未定义行为,因此为了使这行得通,必须在某个地方有强制的顺序
-
所需的强制排序来自于对 std::atomic 变量 data_ready 的操作;内存模型的 happens-before 和 synchronizes-with 关系提供了必要的排序
5.3.1 synchronize -with关系
synchronizes-with 关系基本上来自对原子类型的操作
在适当标记的前提下(默认情况),如果线程 A 存储了一个值,而线程 B 读取了那个值,那么在线程 A 的存储和线程 B 的加载之间存在一个 synchronizes-with 关系
细微之处都在“适当标记”。C++ 内存模型允许对原子类型的操作应用各种排序约束,这就是所指的标记
5.3.2 happens-before关系
happens-before 和 strong-happens-before 指定了哪些操作可以看到其他一些操作的效果。对于单个线程,如果一个操作(A)出现在源代码中另一个操作(B)之前的语句中,那么 A 发生在 B 之前,并且 A strong-happens-before B。
#include <iostream>
void foo(int a, int b){
std::cout << a << "," << b << std::endl;
}
int get_num(){
static int i = 0;
return ++i;
}
int main(){
foo(get_num(), get_num());
}
-
如果这两个操作出现在同一个语句中,通常情况下它们之间没有 happens-before 关系
程序将输出 “1,2” 或 “2,1” -
在某些情况下,单个语句内的操作是有序的,例如使用内置的逗号运算符,或者一个表达式的结果被用作另一个表达式的参数
-
线程间的 happens-before 关系相对简单,并依赖于 synchronizes-with 关系:它也是一个传递关系:如果 A 在线程间发生在 B 之前,而 B 在线程间发生在 C 之前,那么 A 在线程间发生在 C 之前
-
线程间的 strong-happens-before 关系略有不同,但上述描述的两条规则适用。不同之处在于,使用 memory_order_consume 标记的操作参与线程间的 happens-before 关系的建立,但不参与 strong-happens-before 关系
原子读取操作使用 memory_order_consume 标记,表示该操作依赖于之前某个特定写入的结果。这种依赖关系建立了一个线程间的 happens-before 关系,即写入操作 happens-before 读取操作;但这种可见性并不像其他内存排序选项那样强,只确保了读取操作能够观察到特定写入操作的影响,但不确保所有后续操作都受到先前写入操作的约束
5.3.3原子操作的内存排序
六种排序选项,代表了三种模型:
- 顺序一致性排序(memory_order_seq_cst)
- 获取-释放排序(memory_order_consume、memory_order_acquire、memory_order_release 和 memory_order_acq_rel)
- 宽松排序(memory_order_relaxed)
这些不同的内存排序模型在不同的 CPU 架构上可能有不同的成本
顺序一致性排序
就好像所有这些操作都是由单个线程按某种特定顺序执行的:所有线程必须看到相同的操作顺序。操作不能被重新排序;
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); // 安全!z是1,或2
}
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x(){
x.store(true, std::memory_order_seq_cst);
}
void write_y(){
y.store(true, std::memory_order_seq_cst);
}
void read_x_then_y(){
while (!x.load(std::memory_order_seq_cst));
if (y.load(std::memory_order_seq_cst))
++z;
}
void read_y_then_x(){
while (!y.load(std::memory_order_seq_cst));
if (x.load(std::memory_order_seq_cst))
++z;
}
然而,它可能会带来明显的性能损失,因为必须在处理器之间保持操作的总体顺序,可能需要在处理器之间进行大量的(且昂贵的!)同步操作

读x然后读y的,happens-before关系,使得读x看到其值为 true 而读y看到其值为 false 的情况
非顺序一致性的内存排序
-
不再有单一全局的事件顺序。这意味着不同线程可能会看到相同操作的不同视图。而且线程之间不必就事件的顺序达成一致。因为不同的CPU缓存和内部缓冲区可以为同一内存持有不同的值
-
在没有其他排序约束的情况下,唯一的要求是所有线程对每个独立变量的修改顺序达成一致。对不同变量的操作可以在不同线程上出现不同的顺序
宽松排序
- 使用宽松排序执行的原子类型操作不会参与到synchronizes-with关系中
- 唯一的要求是同一线程对单个原子变量的访问不能被重排序
- 在没有任何额外同步的情况下,每个变量的修改顺序是使用memory_order_relaxed的线程之间唯一共享的东西
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x_then_y(){
x.store(true, std::memory_order_relaxed);
y.store(true, std::memory_order_relaxed);
}
void read_y_then_x(){
while (!y.load(std::memory_order_relaxed));
if (x.load(std::memory_order_relaxed)) ++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); //z可能为0
}
变量x和y是不同的,因此对于每个变量上操作产生的值的可见性没有任何顺序保证。不同变量上的relaxed操作可以在遵守它们所受的happens-before关系(例如,在同一个线程内)的情况下自由重排序。它们不会引入synchronizes-with关系。尽管在存储操作之间以及加载操作之间存在happens-before关系,但存储和加载之间并不存在这样的关系,因此加载可能会看到存储操作的无序执行。

std::atomic<int> x(0), y(0), z(0);
std::atomic<bool> go(false);
unsigned const loop_count = 10;
struct read_values{int x, y, z;};
read_values values1[loop_count];
read_values values2[loop_count];
read_values values3[loop_count];
read_values values4[loop_count];
read_values values5[loop_count];
void increment(std::atomic<int>* var_to_inc, read_values* values){
while (!go) std::this_thread::yield();
for (unsigned i = 0; i < loop_count; ++i){
values[i].x = x.load(std::memory_order_relaxed);
values[i].y = y.load(std::memory_order_relaxed);
values[i].z = z.load(std::memory_order_relaxed);
var_to_inc->store(i + 1, std::memory_order_relaxed);
std::this_thread::yield();
}
}
void read_vals(read_values* values){
while (!go) std::this_thread::yield();
for (unsigned i = 0; i < loop_count; ++i){
values[i].x = x.load(std::memory_order_relaxed);
values[i].y = y.load(std::memory_order_relaxed);
values[i].z = z.load(std::memory_order_relaxed);
std::this_thread::yield();
}
}
void print(read_values* v){
for (unsigned i = 0; i < loop_count; ++i){
if (i) std::cout << ",";
std::cout << "(" << v[i].x << "," << v[i].y << "," << v[i].z << ")";
}
std::cout << std::endl;
}
int main(){
std::thread t1(increment, &x, values1);
std::thread t2(increment, &y, values2);
std::thread t3(increment, &z, values3);
std::thread t4(read_vals, values4);
std::thread t5(read_vals, values5);
go = true; t5.join();t4.join();t3.join();t2.join();t1.join();
print(values1);print(values2);print(values3);print(values4);print(values5);
}

理解宽松排序
想象每个原子变量都是一个小隔间里的人,手里拿着一个记事本。在他的记事本上是一系列值。可以打电话给他,要求他给你一个值,或者可以告诉他写下一个新的值。如果告诉他写下一个新值,他会把它写在列表的最底部
就像他为每个人准备了一个标签贴纸
-
假设他的列表一开始是5, 10, 23, 3, 1, 和 2。如果向他要一个值,可能会得到其中任何一个。如果是10,下一次问他要时,他可能会再次给出10,或者后面的任何一个值,但不会是5。如果你告诉他写下42,他会把它加到列表的末尾。如果再次向他要一个数字,他会一直给出“42”,直到他的列表上有另一个数字
-
如果有很多这样的男士(原子变量),手里都有电话和记事本。每个原子变量都有自己的修改顺序(记事本上的值列表),但它们之间没有任何关系。如果每个打电话的人(你、Carl、…)都是一个线程,这就是使用memory_order_relaxed进行操作时的情况
获取-释放排序
- 仍然没有总体顺序,但引入了一些同步。
- 原子加载(load)是获取操作(memory_order_acquire)
- 原子存储(store)是释放操作(memory_order_release)
- 原子读-修改-写操作(如fetch_add()或exchange())可以是获取、释放或两者兼有(memory_order_acq_rel)
- 一个释放操作与读取所写值的获取操作存在synchronizes-with关系
- 不同线程仍然可以看到不同的顺序,但这些顺序是受限的
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)) ++z;
}
void read_y_then_x(){
while (!y.load(std::memory_order_acquire));
if (x.load(std::memory_order_acquire)) ++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); //危险
}

为了看到获取-释放排序的好处,需要考虑同一个线程中的两个存储操作
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x_then_y(){
x.store(true, std::memory_order_relaxed);
y.store(true, std::memory_order_release);
}
void read_y_then_x(){
while (!y.load(std::memory_order_acquire));
if (x.load(std::memory_order_relaxed))
++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); // 安全
}
- 因为存储操作使用了memory_order_release,而加载操作使用了memory_order_acquire,所以存储操作与加载操作是同步的
- 由于它们在同一线程中,存储到变量x的操作发生在存储到变量y的操作之前。
- 因为存储到变量y的操作与从变量y的加载操作同步,所以存储到变量x的操作也发生在从变量y的加载操作之前,并且间接地发生在从变量x的加载操作之前
如果来自y的加载不是在while循环中,则不一定是这种情况,释放操作所存储的值必须能被获取操作看到,才能发挥作用
- 线程a正在运行write_x_then_y,并对隔间x的人说:“请在批次1中代表线程a写入true”
- 接着线程a又对隔间y的人说:“请在批次1中代表线程a写入true作为最后的写入”
- 与此同时,线程b正在运行read_y_then_x。线程b一直向隔间y的男士询问带有批次信息的值,直到他说“true”(并得到写入的线程和批次信息)
- 线程b继续向隔间x的男士询问一个值,但这次他说:“请给我一个值,顺便说一下,我知道线程a的批次1。”
- 现在隔间x的男士必须查看他的列表,寻找线程a在批次1中的最后记录

通过获取-释放排序实现传递同步
- 第一个线程修改一些共享变量,并对其中一个做存储-释放操作
- 第二个线程随后通过加载-获取操作读取受存储-释放操作影响的变量,并对这个变量执行存储-释放操作
- 第三个线程对这个第二个共享变量执行加载-获取操作。只要加载-获取操作能看到存储-释放操作写入的值,以确保存在synchronizes-with关系。这个第三个线程就能读取第一个线程存储的其他变量的值,即使中间的线程没有触碰它们中的任何一个
std::atomic<int> data[5];
std::atomic<bool> sync1(false), sync2(false);
void thread_1(){
data[0].store(42, std::memory_order_relaxed);
data[1].store(97, std::memory_order_relaxed);
data[2].store(17, std::memory_order_relaxed);
data[3].store(-141, std::memory_order_relaxed);
data[4].store(2003, std::memory_order_relaxed);
sync1.store(true, std::memory_order_release); // 1
}
void thread_2(){
while (!sync1.load(std::memory_order_acquire)); // 2
sync2.store(true, std::memory_order_release); // 3
}
void thread_3(){
while (!sync2.load(std::memory_order_acquire)); // 4
assert(data[0].load(std::memory_order_relaxed) == 42);
assert(data[1].load(std::memory_order_relaxed) == 97);
assert(data[2].load(std::memory_order_relaxed) == 17);
assert(data[3].load(std::memory_order_relaxed) == -141);
assert(data[4].load(std::memory_order_relaxed) == 2003);
}
在这种情况下,可以通过在thread_2中使用带有memory_order_acq_rel的读-修改-写操作,将sync1和sync2合并成一个单一的变量。一个选择是使用compare_exchange_strong()来确保只有在看到来自thread_1的存储操作后,值才会被更新
std::atomic<int> sync(0);
void thread_1(){
// ...
sync.store(1, std::memory_order_release);
}
void thread_2(){
int expected = 1;
while (!sync.compare_exchange_strong(expected, 2, std::memory_order_acq_rel))
expected = 1;
}
void thread_3(){
while (sync.load(std::memory_order_acquire) < 2);
// ...
}
read-modify-write操作
使用获取-释放排序和memory_order_consume进行数据依赖
- memory_order_consume很特殊:它为线程间先行发生关系引入了数据依赖性的细微差别
C++17标准明确建议不要使用它 - 如果消费操作(B)读取了由存储操作(A)存储的值,并且存储操作(A)带有memory_order_release、memory_order_acq_rel或memory_order_seq_cst标签,并且加载操作(B)* 带有memory_order_consume标签,那么这个存储操作(A)在依赖排序上先于加载操作(B)
在原子操作加载指向某些数据的指针时。通过在加载时使用memory_order_consume,在之前的存储上使用memory_order_release,可以确保所指向的数据正确同步
struct X {
int i;
std::string s;
};
std::atomic<X*> p;
std::atomic<int> a;
void create_x() {
X* x = new X;
x->i = 42;
x->s = "hello";
a.store(99, std::memory_order_relaxed); //不引入同步约束。
p.store(x, std::memory_order_release); //后续的load操作将看到此次修改
}
void use_x() {
X* x;
while (!(x = p.load(std::memory_order_consume))) {
std::this_thread::sleep(std::chrono::microseconds(1));
}
assert(x->i == 42); // 安全
assert(x->s == “hello”); // 安全
assert(a.load(std::memory_order_relaxed) == 99); // 危险
}
int main() {
std::thread t1(create_x);
std::thread t2(use_x);
t1.join(); t2.join();
}
通过变量x携带了对这些表达式的依赖
-
可以使用std::kill_dependency()来显式地打破依赖链。std::kill_dependency()是一个函数模板,将提供的参数拷贝到返回值中,但在这样做的过程中打破了依赖链
-
告诉编译器它不需要重新读取数组条目的内容
int global_data[]={ … };
std::atomic<int> index;
void f(){
int i=index.load(std::memory_order_consume);
do_something_with(global_data[std::kill_dependency(i)]);
}
std::memory_order_acquire提供了一种更通用的同步保证,适用于大多数同步场景,而std::memory_order_consume则是一种更特殊的内存顺序,它在特定的数据依赖场景下可以提供优化,但需要开发者更加小心地使用以确保正确性和性能
5.3.4释放序列和同步
如果存储操作被标记为memory_order_release、memory_order_acq_rel或memory_order_seq_cst,加载操作被标记为memory_order_consume、memory_order_acquire或memory_order_seq_cst,并且链中的每个操作都加载了前一个操作写入的值,那么这一连串的操作构成了一个释放序列。链中的任何原子读-修改-写操作都可以有任何内存排序(甚至是memory_order_relaxed)
- 使用atomic来作为共享队列中项目数量的计数器
- 第一个fetch_sub()操作参与了释放序列,因此store()操作与第二个fetch_sub()操作同步
std::vector<int> queue_data;
std::atomic<int> count;
void populate_queue(){
unsigned const number_of_items = 20;
queue_data.clear();
for (unsigned i = 0; i < number_of_items; ++i){
queue_data.push_back(i);
}
count.store(number_of_items, std::memory_order_release);
}
void consume_queue_items(){
while (true){
int item_index;
if((item_index=count.fetch_sub(1,std::memory_order_acquire))<=0)
{
wait_for_more_items();
continue;
}
process(queue_data[item_index - 1]);
}
}int main(){
std::thread a(populate_queue);
std::thread b(consume_queue_items);
std::thread c(consume_queue_items);
a.join();b.join();c.join();
}
- 虚线:释放顺序
- 实线:happens-before关系

5.3.5栅栏(Fences)
-
强制执行内存排序约束,而不修改任何数据,通常与使用memory_order_relaxed排序约束的原子操作结合使用。栅栏是全局操作,影响执行栅栏的线程中其他原子操作的顺序。栅栏也通常被称为内存屏障。栅栏限制了宽松操作被重新排序的可能,并引入了新的happens-before和synchronizes-with关系
-
如果一个acquire操作看到一个发生在release栅栏之后的存储操作的结果,那么该栅栏将与该获取操作同步;并且,如果一个发生在acquire栅栏之前的加载操作看到了一个release操作的结果,那么该释放操作将与获取栅栏同步
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x_then_y(){
x.store(true, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_release);
y.store(true, std::memory_order_relaxed);
}
void read_y_then_x(){
while (!y.load(std::memory_order_relaxed));
std::atomic_thread_fence(std::memory_order_acquire);
if (x.load(std::memory_order_relaxed))
++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);
}
重要的是要注意,同步点是栅栏本身。
void write_x_then_y(){
std::atomic_thread_fence(std::memory_order_release);
x.store(true, std::memory_order_relaxed);
y.store(true, std::memory_order_relaxed);
}
这两个操作不再由栅栏分隔,因此(对于其他线程)它们不再有顺序关系。只有当栅栏位于对x的存储操作和对y的存储操作之间时,它才会施加一个顺序
- 在编译时,编译器会根据C++内存模型和指定的内存顺序(如std::memory_order_acquire、std::memory_order_release等)来生成适当的机器代码。编译器必须确保这些内存顺序约束在生成的二进制代码中得到遵守。例如,编译器可能会插入内存栅栏指令到机器代码中,以确保操作的顺序性和可见性
- 在运行时,处理器和操作系统的内存系统也会遵循这些规则,以确保内存操作的正确性和一致性
5.3.6 用原子操作对非原子操作进行排序
如果将代码中的x替换为一个普通的非原子型bool,行为将保证是相同的
bool x = false; // 非atomic
std::atomic<bool> y;
std::atomic<int> z;
void write_x_then_y(){
x = true; // fence前
std::atomic_thread_fence(std::memory_order_release);
y.store(true, std::memory_order_relaxed); //fence后
}
void read_y_then_x(){
while (!y.load(std::memory_order_relaxed));
std::atomic_thread_fence(std::memory_order_acquire);
if (x) ++z; //fence后
}
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.3.7对非原子操作排序
如果一个非原子操作在原子操作之前,并且那个原子操作在另一个线程中的操作之前发生,那么非原子操作也在另一个线程中的那项操作之前发生。这也是C++标准库中更高层同步设施(如互斥锁和条件变量)的基础
std::memory_order_release 确保在特定操作之前的所有内存写入操作对于后续使用 std::memory_order_acquire、std::memory_order_consume 或 std::memory_order_seq_cst 内存序秩序进行的原子读取操作可见
class spinlock_mutex{
private:
std::atomic_flag flag; // 原子标志用于表示锁的状态
public:
// 构造函数初始化标志位为未设置(clear)状态,表示锁未被占用
spinlock_mutex() : flag(ATOMIC_FLAG_INIT) {}
void lock(){// 尝试获取锁,如果锁已被占用,则忙等待(spin-wait)直到锁可用
while (flag.test_and_set(std::memory_order_acquire)); // 如果成功设置(旧值false),表示锁成功
}
void unlock(){// 释放锁,将标志位清除,表示锁现在可用
flag.clear(std::memory_order_release);
}
};
返回 true 表示锁已被其他线程占用,当前线程需要继续循环尝试
在第 2、3 和 4 章中描述的每一种同步机制都将在同步关系方面提供排序保证。这就是能够使用它们来同步数据并提供排序保证的原因。以下是这些设施提供的同步关系:
std::thread
- std::thread 构造函数的完成与在新线程上对提供的函数或可调用对象的调用同步
- 线程的完成与对拥有该线程的 std::thread 对象成功调用 join 的返回同步
std::mutex、std::timed_mutex、std::recursive_mutex和std::recursive_timed_mutex
- 互斥锁的锁顺序:对给定互斥量对象的所有锁定和解锁调用,以及对 try_lock、try_lock_for 或 try_lock_until 的成功调用,形成一个单一的总顺序
- unlock与后续lock的同步
- try_lock等调用失败不参与同步关系

259

被折叠的 条评论
为什么被折叠?



