以下是经过优化排版后的5.3节内容,详细解释了C++中的同步操作和强制排序机制。每个部分都有详细的注释和结构化展示。
文章目录
- 5.3 同步操作和强制排序
- 假设场景
- 示例代码
- 5.3.1 同步发生 (Synchronizes-with)
- 基本思想
- 5.3.2 先行发生 (Happens-before)
- 单线程环境
- 多线程环境
- 5.3.3 内存顺序 (Memory Order)
- 总结
- 5.3.3 原子操作的内存序
- 顺序一致性 (Sequentially Consistent)
- 示例代码
- 性能影响
- 自由序 (Relaxed Ordering)
- 示例代码
- 获取-释放序 (Acquire-Release Ordering)
- 示例代码
- 性能优势
- 非限制操作示例
- 示例代码
- 结果分析
- 总结
- 5.3.3 原子操作的内存序
- 获取-释放序操作会影响释放操作
- 示例代码
- 结果分析
- 获取-释放序传递同步
- 示例代码
- 结果分析
- 合并同步变量
- 示例代码
- memory_order_consume 数据相关性
- 示例代码
- 结果分析
- 总结
- 5.3.4 释放队列与同步
- 示例代码:使用原子操作从队列中读取数据
- 结果分析
- 5.3.5 栅栏(Fences)
- 示例代码:栅栏可以让自由操作变得有序
- 结果分析
- 5.3.6 原子操作对非原子操作的排序
- 示例代码:使用非原子操作执行序列
- 结果分析
- 5.3.7 非原子操作排序
- 示例代码:非原子操作排序
- 结果分析
- 同步工具总结
- `std::thread`
- `std::mutex`, `std::timed_mutex`, `std::recursive_mutex`, `std::recursibe_timed_mutex`
- `std::shared_mutex`, `std::shared_timed_mutex`
- `std::promise`, `std::future`, `std::shared_future`
- `std::async`, `std::future`, `std::shared_future`
- `std::experimental::latch`, `std::experimental::barrier`, `std::experimental::flex_barrier`
- `std::condition_variable`, `std::condition_variable_any`
5.3 同步操作和强制排序
在多线程环境中,确保数据的一致性和正确性至关重要。通过使用原子操作和适当的内存顺序,可以实现线程间的同步和强制排序。
假设场景
假设我们有两个线程:一个用于写入数据(writer_thread
),另一个用于读取数据(reader_thread
)。为了避免竞争条件,写入线程需要设置一个标志来表明数据已经准备好,以便读取线程可以在标志设置后安全地访问数据。
示例代码
#include <vector>
#include <atomic>
#include <iostream>
#include <thread>
#include <chrono>
std::vector<int> data;
std::atomic<bool> data_ready(false);
void reader_thread() {
while (!data_ready.load()) { // 1: 等待数据准备就绪
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
std::cout << "The answer=" << data[0] << "\n"; // 2: 读取数据
}
void writer_thread() {
data.push_back(42); // 3: 写入数据
data_ready.store(true); // 4: 标记数据已准备就绪
}
在这个例子中:
- 等待循环 (
while (!data_ready.load())
) 确保读取线程不会在数据未准备好时访问数据。 - 读取操作 (
data[0]
) 在data_ready
标志被设置为true
后进行。 - 写入操作 (
data.push_back(42)
) 和 标记操作 (data_ready.store(true)
) 确保数据在读取前已经准备好。
尽管每一个数据项都是原子的,但非原子读写操作可能破坏访问顺序,导致未定义行为。为了确保正确的顺序,我们需要理解“先行”和“同步发生”的概念。
5.3.1 同步发生 (Synchronizes-with)
“同步发生”是指两个原子操作之间的关系,其中一个操作必须先于另一个操作完成。这种关系仅存在于原子类型之间。
基本思想
- 原子写操作 W 对变量
x
进行标记,并与对x
的原子读操作同步。 - 读操作要么读到 W 操作写入的内容,要么读到 W 之后同一线程上的原子写操作写入的值,亦或是任意线程对
x
的一系列原子读-改-写操作(如fetch_add()
或compare_exchange_weak()
)。
例如:
std::atomic<int> x(0);
std::atomic<bool> flag(false);
// 线程 A
x.store(42, std::memory_order_relaxed);
flag.store(true, std::memory_order_release);
// 线程 B
while (!flag.load(std::memory_order_acquire)) {
std::this_thread::yield();
}
int value = x.load(std::memory_order_relaxed);
在这个例子中:
- 线程 A 中的
x.store(42)
和flag.store(true)
是原子写操作。 - 线程 B 中的
flag.load()
和x.load()
是原子读操作。 - 当
flag
被设置为true
时,线程 A 的写操作与线程 B 的读操作同步发生。
5.3.2 先行发生 (Happens-before)
“先行发生”是指程序中操作顺序的基本构建块。它规定了某个操作如何影响另一个操作。
单线程环境
在一个单线程环境中,如果操作 A 发生在操作 B 之前,那么 A 就先行于 B。例如:
int get_num() {
static int i = 0;
return ++i;
}
void foo(int a, int b) {
std::cout << a << "," << b << std::endl;
}
int main() {
foo(get_num(), get_num()); // 无序调用 get_num()
}
在这个例子中,get_num()
的调用顺序未指定,输出可能是“1,2”或“2,1”。
多线程环境
在多线程环境中,“先行发生”关系依赖于同步关系:
- 如果操作 A 在一个线程上,并且该线程先行于另一个线程上的操作 B,那么 A 就先行于 B。
- 如果操作 A 与另一个线程上的操作 B 同步,那么 A 就线程间先行于 B。
传递关系:如果 A 先行于 B,并且 B 先行于 C,那么 A 就先行于 C。
示例代码:
std::atomic<bool> ready(false);
void thread_a() {
data.push_back(42); // 写入数据
ready.store(true, std::memory_order_release); // 设置标志
}
void thread_b() {
while (!ready.load(std::memory_order_acquire)) { // 等待标志
std::this_thread::yield();
}
std::cout << "The answer=" << data[0] << "\n"; // 读取数据
}
在这个例子中:
thread_a
中的写操作先行于ready.store(true)
。ready.load()
先行于thread_b
中的读操作。- 因此,写操作先行于读操作。
5.3.3 内存顺序 (Memory Order)
内存顺序决定了原子操作的行为以及它们与其他操作的关系。常见的内存顺序包括:
- memory_order_relaxed: 不保证任何顺序。
- memory_order_consume: 依赖于当前线程的操作结果。
- memory_order_acquire: 确保后续操作不会被重排序到当前操作之前。
- memory_order_release: 确保之前的操作不会被重排序到当前操作之后。
- memory_order_acq_rel: 结合 acquire 和 release 语义。
- memory_order_seq_cst: 提供全局顺序一致性。
示例代码:
std::atomic<int> x(0);
std::atomic<int> y(0);
// 线程 A
x.store(1, std::memory_order_relaxed);
y.store(1, std::memory_order_release);
// 线程 B
while (y.load(std::memory_order_acquire) == 0) {
std::this_thread::yield();
}
int value = x.load(std::memory_order_relaxed);
在这个例子中:
y.store(1, std::memory_order_release)
确保之前的x.store(1)
不会被重排序到其后。y.load(std::memory_order_acquire)
确保后续的x.load()
不会读取到旧值。
总结
通过理解和应用“同步发生”和“先行发生”关系,我们可以确保多线程程序中的数据一致性和正确性。合理选择内存顺序也是至关重要的,它可以帮助我们控制操作的顺序并避免潜在的竞争条件。
这些规则是编写高效、安全的多线程程序的基础,能够帮助我们在复杂的并发环境中管理数据共享和同步。希望这些解释和示例能帮助你更好地理解和应用这些概念。
以下是经过优化排版后的5.3.3节内容,详细解释了C++中的原子操作内存序。每个部分都有详细的注释和结构化展示。
5.3.3 原子操作的内存序
在多线程编程中,内存序(Memory Order)决定了原子操作的行为及其与其他操作的关系。C++提供了六种不同的内存序选项:
- memory_order_relaxed
- memory_order_consume
- memory_order_acquire
- memory_order_release
- memory_order_acq_rel
- memory_order_seq_cst
除非为特定的操作指定一个序列选项,默认内存序是 memory_order_seq_cst
(顺序一致性)。尽管有六个选项,但它们代表三种主要的内存模型:顺序一致性、获取-释放序和自由序。
顺序一致性 (Sequentially Consistent)
默认内存序 memory_order_seq_cst
是最简单且最容易理解的内存序。它确保所有线程看到的操作顺序是一致的。
示例代码
#include <atomic>
#include <thread>
#include <cassert>
std::atomic<bool> x(false), y(false);
std::atomic<int> z(0);
void write_x() {
x.store(true, std::memory_order_seq_cst); // 1: 写入x
}
void write_y() {
y.store(true, std::memory_order_seq_cst); // 2: 写入y
}
void read_x_then_y() {
while (!x.load(std::memory_order_seq_cst)); // 等待x变为true
if (y.load(std::memory_order_seq_cst)) { // 3: 检查y是否为true
++z;
}
}
void read_y_then_x() {
while (!y.load(std::memory_order_seq_cst)); // 等待y变为true
if (x.load(std::memory_order_seq_cst)) { // 4: 检查x是否为true
++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); // 断言z不为0
}
在这个例子中:
write_x
和write_y
分别设置x
和y
为true
。read_x_then_y
和read_y_then_x
分别等待x
和y
变为true
,然后检查另一个变量并增加z
的值。
由于使用了 memory_order_seq_cst
,所有的操作都保持全局一致的顺序,因此断言不会触发。
性能影响
虽然顺序一致性是最直观的内存序,但在多核系统上会带来较大的性能开销,因为它需要在多个处理器之间进行同步操作。
自由序 (Relaxed Ordering)
memory_order_relaxed
提供最小的同步保证,适用于不需要严格顺序的应用场景。
示例代码
#include <atomic>
#include <thread>
#include <cassert>
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: 写入x
y.store(true, std::memory_order_relaxed); // 2: 写入y
}
void read_y_then_x() {
while (!y.load(std::memory_order_relaxed)); // 3: 等待y变为true
if (x.load(std::memory_order_relaxed)) { // 4: 检查x是否为true
++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); // 断言可能会触发
}
在这个例子中:
write_x_then_y
先写入x
,然后写入y
,但没有顺序保证。read_y_then_x
等待y
变为true
,然后检查x
是否为true
并增加z
的值。
由于使用了 memory_order_relaxed
,读取操作可能看不到最新的写入值,因此断言可能会触发。
获取-释放序 (Acquire-Release Ordering)
获取-释放序提供了比自由序更强的同步保证,但不像顺序一致性那样严格。
示例代码
#include <atomic>
#include <thread>
#include <cassert>
std::atomic<bool> x(false), y(false);
std::atomic<int> z(0);
void write_x() {
x.store(true, std::memory_order_release); // 1: 写入x
}
void write_y() {
y.store(true, std::memory_order_release); // 2: 写入y
}
void read_x_then_y() {
while (!x.load(std::memory_order_acquire)); // 等待x变为true
if (y.load(std::memory_order_acquire)) { // 3: 检查y是否为true
++z;
}
}
void read_y_then_x() {
while (!y.load(std::memory_order_acquire)); // 等待y变为true
if (x.load(std::memory_order_acquire)) { // 4: 检查x是否为true
++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); // 断言可能会触发
}
在这个例子中:
write_x
和write_y
使用memory_order_release
标记写入操作。read_x_then_y
和read_y_then_x
使用memory_order_acquire
进行读取操作。
获取-释放序确保了一个线程的释放操作与另一个线程的获取操作同步,从而提供了一定程度的顺序保证,但仍不如顺序一致性那么严格。
性能优势
获取-释放序通常比顺序一致性更高效,因为它只需要在相关线程之间进行同步,而不是全局同步。
非限制操作示例
为了更好地理解非限制操作,考虑以下多线程示例:
示例代码
#include <thread>
#include <atomic>
#include <iostream>
std::atomic<int> x(0), y(0), z(0); // 1: 全局原子变量
std::atomic<bool> go(false); // 2: 同步信号
unsigned const loop_count = 10;
struct read_values {
int x, y, z;
};
read_values values1[loop_count], values2[loop_count], values3[loop_count], values4[loop_count], values5[loop_count];
void increment(std::atomic<int>* var_to_inc, read_values* values) {
while (!go) std::this_thread::yield(); // 3: 等待信号
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); // 4: 更新变量
std::this_thread::yield();
}
}
void read_vals(read_values* values) {
while (!go) std::this_thread::yield(); // 5: 等待信号
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() {
x = 0;
y = 0;
z = 0;
go = false;
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; // 6: 开始执行主循环的信号
t5.join();
t4.join();
t3.join();
t2.join();
t1.join();
print(values1); // 7: 打印最终结果
print(values2);
print(values3);
print(values4);
print(values5);
}
在这个例子中:
- 三个全局原子变量
x
,y
,z
和一个同步信号go
。 - 每个线程循环10次,使用
memory_order_relaxed
读取三个原子变量的值,并存储在一个数组中。 - 三个线程每次通过循环更新其中一个原子变量,剩下的两个线程负责读取。
输出示例:
(0,0,0),(1,0,0),(2,0,0),(3,0,0),(4,0,0),(5,7,0),(6,7,8),(7,9,8),(8,9,8),(9,9,10)
(0,0,0),(0,1,0),(0,2,0),(1,3,5),(8,4,5),(8,5,5),(8,6,6),(8,7,9),(10,8,9),(10,9,10)
(0,0,0),(0,0,1),(0,0,2),(0,0,3),(0,0,4),(0,0,5),(0,0,6),(0,0,7),(0,0,8),(0,0,9)
(1,3,0),(2,3,0),(2,4,1),(3,6,4),(3,9,5),(5,10,6),(5,10,8),(5,10,10),(9,10,10),(10,10,10)
(0,0,0),(0,0,0),(0,0,0),(6,3,7),(6,5,7),(7,7,7),(7,8,7),(8,8,7),(8,8,9),(8,8,9)
结果分析
- 第一组值中
x
增加1,第二组值中y
增加1,第三组中z
增加1。 x
元素只在给定集中增加,y
和z
也一样,但不是均匀增加,并且每个线程中的相对顺序不同。- 线程3看不到
x
或y
的任何更新,但它能看到z
的更新。
总结
通过理解和应用不同的内存序选项,可以在多线程编程中实现高效的同步和强制排序。每种内存序都有其适用场景:
- 顺序一致性 (
memory_order_seq_cst
):最简单且直观,但性能开销较大。 - 自由序 (
memory_order_relaxed
):性能最佳,但缺乏严格的顺序保证。 - 获取-释放序 (
memory_order_acquire
和memory_order_release
):提供了较强的同步保证,同时保持较高的性能。
选择合适的内存序可以帮助你在保证程序正确性的同时,最大化性能。希望这些解释和示例能帮助你更好地理解和应用这些概念。
以下是经过优化排版后的5.3.3节内容,详细解释了C++中的原子操作内存序,特别是获取-释放序的操作及其传递同步特性。每个部分都有详细的注释和结构化展示。
5.3.3 原子操作的内存序
在多线程编程中,内存序(Memory Order)决定了原子操作的行为及其与其他操作的关系。C++提供了六种不同的内存序选项:
- memory_order_relaxed
- memory_order_consume
- memory_order_acquire
- memory_order_release
- memory_order_acq_rel
- memory_order_seq_cst
除非为特定的操作指定一个序列选项,默认内存序是 memory_order_seq_cst
(顺序一致性)。尽管有六个选项,但它们代表三种主要的内存模型:顺序一致性、获取-释放序和自由序。
获取-释放序操作会影响释放操作
示例代码
#include <atomic>
#include <thread>
#include <cassert>
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: 写入x
y.store(true, std::memory_order_release); // 2: 写入y
}
void read_y_then_x() {
while (!y.load(std::memory_order_acquire)); // 3: 等待y变为true
if (x.load(std::memory_order_relaxed)) { // 4: 检查x是否为true
++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); // 断言不会触发
}
在这个例子中:
write_x_then_y
先写入x
,然后写入y
。read_y_then_x
等待y
变为true
,然后检查x
是否为true
并增加z
的值。
由于使用了 memory_order_release
和 memory_order_acquire
,读取操作会看到最新的写入值,因此断言不会触发。
结果分析
- 存储
x
使用的是memory_order_relaxed
,没有顺序保证。 - 存储
y
使用的是memory_order_release
,确保后续的加载操作能看到这个值。 - 加载
y
使用的是memory_order_acquire
,确保能看到之前的所有释放操作。
因此,存储 x
的操作先行于存储 y
的操作,并且扩展到对 x
的读取操作,使得 z
最终不为零。
获取-释放序传递同步
为了考虑传递顺序,至少需要三个线程。第一个线程用来修改共享变量,第二个线程使用“加载-获取”读取由“存储-释放”操作过的变量,并且再对第二个变量进行“存储-释放”操作。最后,由第三个线程通过“加载-获取”读取第二个共享变量,并提供“加载-获取”操作来读取被“存储-释放”操作写入的值。
示例代码
#include <atomic>
#include <thread>
#include <cassert>
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. 设置sync1
}
void thread_2() {
while (!sync1.load(std::memory_order_acquire)); // 2. 直到sync1设置后,循环结束
sync2.store(true, std::memory_order_release); // 3. 设置sync2
}
void thread_3() {
while (!sync2.load(std::memory_order_acquire)); // 4. 直到sync2设置后,循环结束
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);
}
int main() {
std::thread t1(thread_1);
std::thread t2(thread_2);
std::thread t3(thread_3);
t1.join();
t2.join();
t3.join();
}
在这个例子中:
thread_1
将数据存储到data
中,并设置sync1
。thread_2
等待sync1
被设置后,设置sync2
。thread_3
等待sync2
被设置后,读取data
中的数据并进行断言。
由于 thread_2
只接触到 sync1
和 sync2
,对于 thread_1
和 thread_3
的同步就足够了,这能保证断言不会触发。
结果分析
thread_1
将数据存储到data
中先行于存储sync1
。- 对
sync1
的加载最终会看到thread_1
存储的值。 thread_3
的加载操作位于存储sync2
操作的前面。- 存储
sync2
因此先行于thread_3
的加载,从而保证断言都不会触发。
合并同步变量
可以将 sync1
和 sync2
通过在 thread_2
中使用“读-改-写”操作 (memory_order_acq_rel
) 合并成一个独立的变量。其中会使用 compare_exchange_strong()
来保证 thread_1
对变量只进行一次更新。
示例代码
#include <atomic>
#include <thread>
#include <cassert>
std::atomic<int> data[5];
std::atomic<int> sync(0);
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);
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);
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);
}
int main() {
std::thread t1(thread_1);
std::thread t2(thread_2);
std::thread t3(thread_3);
t1.join();
t2.join();
t3.join();
}
在这个例子中:
thread_1
将数据存储到data
中并设置sync
为1
。thread_2
使用compare_exchange_strong
将sync
从1
更新为2
。thread_3
等待sync
大于等于2
后,读取data
中的数据并进行断言。
使用 memory_order_acq_rel
的“读-改-写”操作,选择语义非常重要。例子中,想要同时进行获取和释放的语义,所以 memory_order_acq_rel
是一个不错的选择。
memory_order_consume 数据相关性
memory_order_consume
是一种特殊的内存序,完全依赖于数据,并展示了与线程间先行关系的不同之处。尽管它在C++17中不推荐使用,但仍有必要了解其概念。
示例代码
#include <atomic>
#include <thread>
#include <cassert>
#include <string>
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); // 1
p.store(x, std::memory_order_release); // 2
}
void use_x() {
X* x;
while (!(x = p.load(std::memory_order_consume))) // 3
std::this_thread::sleep_for(std::chrono::microseconds(1));
assert(x->i == 42); // 4
assert(x->s == "hello"); // 5
assert(a.load(std::memory_order_relaxed) == 99); // 6
}
int main() {
std::thread t1(create_x);
std::thread t2(use_x);
t1.join();
t2.join();
}
在这个例子中:
create_x
创建一个X
结构体实例,并将其指针存储在p
中。use_x
等待p
被设置为非空值,然后访问X
结构体的成员。
由于 memory_order_consume
的使用,X
结构体中的数据成员所在的断言语句不会被触发。然而,加载 a
的断言不能确定是否触发,因为这个操作标记为 memory_order_relaxed
,并不依赖于 p
的加载操作。
结果分析
- 存储
a
在存储p
之前,并且存储p
的操作标记为memory_order_release
。 - 加载
p
的操作标记为memory_order_consume
,因此存储p
仅先行那些需要加载p
的操作。 - 对
x
变量操作的表达式对加载p
的操作携带有依赖,所以X
结构体中数据成员所在的断言语句不会被触发。
总结
通过理解和应用不同的内存序选项,可以在多线程编程中实现高效的同步和强制排序。每种内存序都有其适用场景:
- 顺序一致性 (
memory_order_seq_cst
):最简单且直观,但性能开销较大。 - 自由序 (
memory_order_relaxed
):性能最佳,但缺乏严格的顺序保证。 - 获取-释放序 (
memory_order_acquire
和memory_order_release
):提供了较强的同步保证,同时保持较高的性能。 - memory_order_consume:依赖于数据的相关性,但在实际应用中不推荐使用。
选择合适的内存序可以帮助你在保证程序正确性的同时,最大化性能。希望这些解释和示例能帮助你更好地理解和应用这些概念。
以下是经过优化排版后的5.3.4至5.3.7节内容,详细解释了C++中的释放队列、栅栏操作及其对非原子操作的排序。每个部分都有详细的注释和结构化展示。
5.3.4 释放队列与同步
在多线程编程中,释放队列(Release Sequence)是确保不同线程之间正确同步的重要机制。通过使用适当的内存序标记,可以保证存储和加载操作之间的同步关系。
示例代码:使用原子操作从队列中读取数据
#include <atomic>
#include <thread>
#include <vector>
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); // 1 初始化存储
}
void consume_queue_items() {
while (true) {
int item_index;
if ((item_index = count.fetch_sub(1, std::memory_order_acquire)) <= 0) { // 2 “读-改-写”操作
wait_for_more_items(); // 3 等待更多元素
continue;
}
process(queue_data[item_index - 1]); // 4 安全读取queue_data
}
}
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();
}
结果分析
populate_queue
函数初始化共享队列并设置计数器。consume_queue_items
函数从队列中获取元素,并处理它们。- 使用
memory_order_release
和memory_order_acquire
确保存储和加载操作之间的同步。
当只有一个消费者线程时,一切正常。如果有两个消费者线程,第二个线程会看到第一个线程修改的值,从而避免条件竞争。
5.3.5 栅栏(Fences)
栅栏操作是对内存序列进行约束的全局操作,限制编译器或硬件对指令的重新排序。栅栏操作可以确保特定的操作顺序。
示例代码:栅栏可以让自由操作变得有序
#include <atomic>
#include <thread>
#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
std::atomic_thread_fence(std::memory_order_release); // 2
y.store(true, std::memory_order_relaxed); // 3
}
void read_y_then_x() {
while (!y.load(std::memory_order_relaxed)); // 4
std::atomic_thread_fence(std::memory_order_acquire); // 5
if (x.load(std::memory_order_relaxed)) // 6
++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); // 7
}
结果分析
write_x_then_y
函数使用释放栅栏确保x
和y
的存储顺序。read_y_then_x
函数使用获取栅栏确保y
和x
的加载顺序。- 栅栏操作确保了存储和加载操作之间的同步关系,避免了条件竞争。
如果将存储和加载操作标记为 memory_order_relaxed
,则需要栅栏来强制执行顺序。
5.3.6 原子操作对非原子操作的排序
即使使用非原子变量,也可以通过栅栏操作确保操作的顺序性。
示例代码:使用非原子操作执行序列
#include <atomic>
#include <thread>
#include <assert.h>
bool x = false; // x现在是一个非原子变量
std::atomic<bool> y(false);
std::atomic<int> z(0);
void write_x_then_y() {
x = true; // 1 在栅栏前存储x
std::atomic_thread_fence(std::memory_order_release);
y.store(true, std::memory_order_relaxed); // 2 在栅栏后存储y
}
void read_y_then_x() {
while (!y.load(std::memory_order_relaxed)); // 3 在#2写入前,持续等待
std::atomic_thread_fence(std::memory_order_acquire);
if (x) // 4 这里读取到的值,是#1中写入
++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 断言将不会触发
}
结果分析
write_x_then_y
函数使用释放栅栏确保x
和y
的存储顺序。read_y_then_x
函数使用获取栅栏确保y
和x
的加载顺序。- 尽管
x
是一个非原子变量,但栅栏操作仍然确保了操作的顺序性。
5.3.7 非原子操作排序
通过使用栅栏操作,可以对非原子操作进行排序,确保其顺序性和同步性。
示例代码:非原子操作排序
#include <atomic>
#include <thread>
#include <cassert>
bool x = false; // 非原子变量
std::atomic<bool> y(false);
std::atomic<int> z(0);
void write_x_then_y() {
x = true; // 1 在栅栏前存储x
std::atomic_thread_fence(std::memory_order_release);
y.store(true, std::memory_order_relaxed); // 2 在栅栏后存储y
}
void read_y_then_x() {
while (!y.load(std::memory_order_relaxed)); // 3 在#2写入前,持续等待
std::atomic_thread_fence(std::memory_order_acquire);
if (x) // 4 这里读取到的值,是#1中写入
++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 断言将不会触发
}
结果分析
write_x_then_y
函数使用释放栅栏确保x
和y
的存储顺序。read_y_then_x
函数使用获取栅栏确保y
和x
的加载顺序。- 即使
x
是一个非原子变量,栅栏操作仍然确保了操作的顺序性和同步性。
同步工具总结
以下是一些常用的同步工具及其作用:
std::thread
- 构造函数与调用函数或新线程的可调用对象间的同步。
- 对
std::thread
对象调用join
可以和对应的线程进行同步。
std::mutex
, std::timed_mutex
, std::recursive_mutex
, std::recursibe_timed_mutex
- 对给定互斥量对象调用
lock
和unlock
,以及对try_lock
、try_lock_for
或try_lock_until
,会形成该互斥量的锁序。 - 对给定的互斥量调用
unlock
,需要在调用lock
或成功调用try_lock
、try_lock_for
或try_lock_until
之后,这样才符合互斥量的锁序。
std::shared_mutex
, std::shared_timed_mutex
- 对给定互斥量对象调用
lock
、unlock
、lock_shared
和unlock_shared
,以及对try_lock
、try_lock_for
、try_lock_until
、try_lock_shared
、try_lock_shared_for
或try_lock_shared_until
的成功调用,会形成该互斥量的锁序。
std::promise
, std::future
, std::shared_future
- 成功调用
std::promise
对象的set_value
或set_exception
与成功的调用wait
或get
之间同步。 - 成功调用
std::packaged_task
对象的函数操作符与成功的调用wait
或get
之间同步。
std::async
, std::future
, std::shared_future
- 使用
std::launch::async
策略性的通过std::async
启动线程执行任务与成功的调用wait
和get
之间是同步的。 - 使用
std::launch::deferred
策略性的通过std::async
启动任务与成功的调用wait
和get
之间是同步的。
std::experimental::latch
, std::experimental::barrier
, std::experimental::flex_barrier
- 对
std::experimental::latch
实例调用count_down
或count_down_and_wait
与在该对象上成功的调用wait
或count_down_and_wait
之间是同步的。 - 对
std::experimental::barrier
实例调用arrive_and_wait
或arrive_and_drop
与在该对象上随后成功完成的arrive_and_wait
之间是同步的。
std::condition_variable
, std::condition_variable_any
- 条件变量不提供任何同步关系,所有同步都由互斥量提供。
这些同步工具提供了丰富的功能,帮助开发者在多线程环境中实现正确的同步和顺序控制。希望这些解释和示例能帮助你更好地理解和应用这些概念。