C++并发编程指南08

以下是经过优化排版后的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++提供了六种不同的内存序选项:

  1. memory_order_relaxed
  2. memory_order_consume
  3. memory_order_acquire
  4. memory_order_release
  5. memory_order_acq_rel
  6. 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变为trueif (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变为trueif (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_xwrite_y 分别设置 xytrue
  • read_x_then_yread_y_then_x 分别等待 xy 变为 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: 写入xy.store(true, std::memory_order_relaxed);  // 2: 写入y
}void read_y_then_x() {while (!y.load(std::memory_order_relaxed));  // 3: 等待y变为trueif (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变为trueif (y.load(std::memory_order_acquire)) {  // 3: 检查y是否为true++z;}
}void read_y_then_x() {while (!y.load(std::memory_order_acquire));  // 等待y变为trueif (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_xwrite_y 使用 memory_order_release 标记写入操作。
  • read_x_then_yread_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 元素只在给定集中增加,yz 也一样,但不是均匀增加,并且每个线程中的相对顺序不同。
  • 线程3看不到 xy 的任何更新,但它能看到 z 的更新。

总结

通过理解和应用不同的内存序选项,可以在多线程编程中实现高效的同步和强制排序。每种内存序都有其适用场景:

  • 顺序一致性 (memory_order_seq_cst):最简单且直观,但性能开销较大。
  • 自由序 (memory_order_relaxed):性能最佳,但缺乏严格的顺序保证。
  • 获取-释放序 (memory_order_acquirememory_order_release):提供了较强的同步保证,同时保持较高的性能。

选择合适的内存序可以帮助你在保证程序正确性的同时,最大化性能。希望这些解释和示例能帮助你更好地理解和应用这些概念。
以下是经过优化排版后的5.3.3节内容,详细解释了C++中的原子操作内存序,特别是获取-释放序的操作及其传递同步特性。每个部分都有详细的注释和结构化展示。


5.3.3 原子操作的内存序

在多线程编程中,内存序(Memory Order)决定了原子操作的行为及其与其他操作的关系。C++提供了六种不同的内存序选项:

  1. memory_order_relaxed
  2. memory_order_consume
  3. memory_order_acquire
  4. memory_order_release
  5. memory_order_acq_rel
  6. 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: 写入xy.store(true, std::memory_order_release);  // 2: 写入y
}void read_y_then_x() {while (!y.load(std::memory_order_acquire));  // 3: 等待y变为trueif (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_releasememory_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 只接触到 sync1sync2,对于 thread_1thread_3 的同步就足够了,这能保证断言不会触发。

结果分析
  • thread_1 将数据存储到 data 中先行于存储 sync1
  • sync1 的加载最终会看到 thread_1 存储的值。
  • thread_3 的加载操作位于存储 sync2 操作的前面。
  • 存储 sync2 因此先行于 thread_3 的加载,从而保证断言都不会触发。

合并同步变量

可以将 sync1sync2 通过在 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 中并设置 sync1
  • thread_2 使用 compare_exchange_strongsync1 更新为 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);  // 1p.store(x, std::memory_order_release);   // 2
}void use_x() {X* x;while (!(x = p.load(std::memory_order_consume)))  // 3std::this_thread::sleep_for(std::chrono::microseconds(1));assert(x->i == 42);  // 4assert(x->s == "hello");  // 5assert(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_acquirememory_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_releasememory_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);  // 1std::atomic_thread_fence(std::memory_order_release);  // 2y.store(true, std::memory_order_relaxed);  // 3
}void read_y_then_x() {while (!y.load(std::memory_order_relaxed));  // 4std::atomic_thread_fence(std::memory_order_acquire);  // 5if (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 函数使用释放栅栏确保 xy 的存储顺序。
  • read_y_then_x 函数使用获取栅栏确保 yx 的加载顺序。
  • 栅栏操作确保了存储和加载操作之间的同步关系,避免了条件竞争。

如果将存储和加载操作标记为 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 在栅栏前存储xstd::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 函数使用释放栅栏确保 xy 的存储顺序。
  • read_y_then_x 函数使用获取栅栏确保 yx 的加载顺序。
  • 尽管 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 在栅栏前存储xstd::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 函数使用释放栅栏确保 xy 的存储顺序。
  • read_y_then_x 函数使用获取栅栏确保 yx 的加载顺序。
  • 即使 x 是一个非原子变量,栅栏操作仍然确保了操作的顺序性和同步性。

同步工具总结

以下是一些常用的同步工具及其作用:

std::thread
  • 构造函数与调用函数或新线程的可调用对象间的同步。
  • std::thread 对象调用 join 可以和对应的线程进行同步。
std::mutex, std::timed_mutex, std::recursive_mutex, std::recursibe_timed_mutex
  • 对给定互斥量对象调用 lockunlock,以及对 try_locktry_lock_fortry_lock_until,会形成该互斥量的锁序。
  • 对给定的互斥量调用 unlock,需要在调用 lock 或成功调用 try_locktry_lock_fortry_lock_until 之后,这样才符合互斥量的锁序。
std::shared_mutex, std::shared_timed_mutex
  • 对给定互斥量对象调用 lockunlocklock_sharedunlock_shared,以及对 try_locktry_lock_fortry_lock_untiltry_lock_sharedtry_lock_shared_fortry_lock_shared_until 的成功调用,会形成该互斥量的锁序。
std::promise, std::future, std::shared_future
  • 成功调用 std::promise 对象的 set_valueset_exception 与成功的调用 waitget 之间同步。
  • 成功调用 std::packaged_task 对象的函数操作符与成功的调用 waitget 之间同步。
std::async, std::future, std::shared_future
  • 使用 std::launch::async 策略性的通过 std::async 启动线程执行任务与成功的调用 waitget 之间是同步的。
  • 使用 std::launch::deferred 策略性的通过 std::async 启动任务与成功的调用 waitget 之间是同步的。
std::experimental::latch, std::experimental::barrier, std::experimental::flex_barrier
  • std::experimental::latch 实例调用 count_downcount_down_and_wait 与在该对象上成功的调用 waitcount_down_and_wait 之间是同步的。
  • std::experimental::barrier 实例调用 arrive_and_waitarrive_and_drop 与在该对象上随后成功完成的 arrive_and_wait 之间是同步的。
std::condition_variable, std::condition_variable_any
  • 条件变量不提供任何同步关系,所有同步都由互斥量提供。

这些同步工具提供了丰富的功能,帮助开发者在多线程环境中实现正确的同步和顺序控制。希望这些解释和示例能帮助你更好地理解和应用这些概念。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/11156.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

7.攻防世界fileclude

题目描述 进入题目页面如下 看到题目提示应该为文件包含漏洞 解释上述代码 // 输出提示信息&#xff1a;错误的方式&#xff01; WRONG WAY! <?php // 包含名为 "flag.php" 的文件&#xff0c;通常这个文件里可能包含重要的敏感信息&#xff0c;如 flag inclu…

Manacher 最长回文子串

方法&#xff1a;求字符串的 #include<bits/stdc.h> using namespace std; using lllong long; const int N1e69; char s[N]; int p[N];int main() {cin>>s1;int nstrlen(s1);s[0]^;s[2*n2]$; for(int i2*n1;i>1;i--){s[i](i&1)?#:s[i>>1];//右移表示…

5.4.2 结构化设计方法+结构化程序设计方法

文章目录 结构化设计方法结构化程序设计方法 结构化设计方法 结构化设计是将通过结构化分析得到的数据流图转换成软件体系结构。可用使用结构图描述结构化设计&#xff0c;结构图由模块、数据和调用组成。模块是指有功能&#xff0c;且可通过模块名调用的程序语句。其内部特征包…

ArkTS语言介绍

文章目录 一、基本知识声明类型运算符语句函数函数声明可选参数Rest参数返回类型函数的作用域函数调用函数类型箭头函数(又名Lambda函数)闭包函数重载类字段方法构造函数可见性修饰符对象字面量抽象类接口接口属性接口继承抽象类和接口泛型类型和函数泛型类和接口泛型约束泛型…

【2025年最新版】Java JDK安装、环境配置教程 (图文非常详细)

文章目录 【2025年最新版】Java JDK安装、环境配置教程 &#xff08;图文非常详细&#xff09;1. JDK介绍2. 下载 JDK3. 安装 JDK4. 配置环境变量5. 验证安装6. 创建并测试简单的 Java 程序6.1 创建 Java 程序&#xff1a;6.2 编译和运行程序&#xff1a;6.3 在显示或更改文件的…

71.在 Vue 3 中使用 OpenLayers 实现按住 Shift 拖拽、旋转和缩放效果

前言 在前端开发中&#xff0c;地图功能是一个常见的需求。OpenLayers 是一个强大的开源地图库&#xff0c;支持多种地图源和交互操作。本文将介绍如何在 Vue 3 中集成 OpenLayers&#xff0c;并实现按住 Shift 键拖拽、旋转和缩放地图的效果。 实现效果 按住 Shift 键&#…

【数据结构】_复杂度

目录 1. 算法效率 2. 时间复杂度 2.1 时间复杂度概念 2.2 准确的时间复杂度函数式 2.3 大O渐进表示法 2.4 时间复杂度的常见量级 2.5 时间复杂度示例 3. 空间复杂度 3.1 空间复杂度概念 3.2 空间复杂度示例 1. 算法效率 一般情况下&#xff0c;衡量一个算法的好坏是…

十分钟快速上手 markdown

前言 本人利用寒假期间&#xff0c;将自己所学的markdown的知识&#xff0c;以及将自己常用的一些操作和注意事项记录下来&#xff0c;希望能够帮助大家 一、markdown是什么 Markdown 是一种轻量级标记语言&#xff0c;说白了就是可以让你利用最简单的语法达到最好的排版效果…

一文讲解Java中的ArrayList和LinkedList

ArrayList和LinkedList有什么区别&#xff1f; ArrayList 是基于数组实现的&#xff0c;LinkedList 是基于链表实现的。 二者用途有什么不同&#xff1f; 多数情况下&#xff0c;ArrayList更利于查找&#xff0c;LinkedList更利于增删 由于 ArrayList 是基于数组实现的&#…

Python 梯度下降法(五):Adam Optimize

文章目录 Python 梯度下降法&#xff08;五&#xff09;&#xff1a;Adam Optimize一、数学原理1.1 介绍1.2 符号说明1.3 实现流程 二、代码实现2.1 函数代码2.2 总代码2.3 遇到的问题2.4 算法优化 三、优缺点3.1 优点3.2 缺点 四、相关链接 Python 梯度下降法&#xff08;五&a…

【上篇】-分两篇步骤介绍-如何用topview生成和自定义数字人-关于AI的使用和应用-如何生成数字人-优雅草卓伊凡-如何生成AI数字人

【上篇】-分两篇步骤介绍-如何用topview生成和自定义数字人-关于AI的使用和应用-如何生成数字人-优雅草卓伊凡-如何生成AI数字人 背景 AI数字人有很多应用目前&#xff0c;本文做如何生成数字人&#xff0c;因为后续就连我们公司自己也会有很多关于AI数字人的使用&#xff0c…

MapReduce简单应用(一)——WordCount

目录 1. 执行过程1.1 分割1.2 Map1.3 Combine1.4 Reduce 2. 代码和结果2.1 pom.xml中依赖配置2.2 工具类util2.3 WordCount2.4 结果 参考 1. 执行过程 假设WordCount的两个输入文本text1.txt和text2.txt如下。 Hello World Bye WorldHello Hadoop Bye Hadoop1.1 分割 将每个文…

tensorboard的基本使用及案例

TensorBoard 是一个可视化工具&#xff0c;用于展示机器学习模型的训练过程和结果。以下是 TensorBoard 的基本使用方法及一些案例。 基本使用 安装 安装 TensorBoard&#xff1a; pip install tensorboard 如果使用 PyTorch&#xff0c;还需要安装 torch 和 torchvision&…

【ArcGIS遇上Python】批量提取多波段影像至单个波段

本案例基于ArcGIS python,将landsat影像的7个波段影像数据,批量提取至单个波段。 相关阅读:【ArcGIS微课1000例】0141:提取多波段影像中的单个波段 文章目录 一、数据准备二、效果比对二、python批处理1. 编写python代码2. 运行代码一、数据准备 实验数据及完整的python位…

吴恩达深度学习——超参数调试

内容来自https://www.bilibili.com/video/BV1FT4y1E74V&#xff0c;仅为本人学习所用。 文章目录 超参数调试调试选择范围 Batch归一化公式整合 Softmax 超参数调试 调试 目前学习的一些超参数有学习率 α \alpha α&#xff08;最重要&#xff09;、动量梯度下降法 β \bet…

Alibaba开发规范_编程规约之命名风格

文章目录 命名风格的基本原则1. 命名不能以下划线或美元符号开始或结束2. 严禁使用拼音与英文混合或直接使用中文3. 类名使用 UpperCamelCase 风格&#xff0c;但以下情形例外&#xff1a;DO / BO / DTO / VO / AO / PO / UID 等4. 方法名、参数名、成员变量、局部变量使用 low…

从0开始,来看看怎么去linux排查Java程序故障

一&#xff0c;前提准备 最基本前提&#xff1a;你需要有liunx环境&#xff0c;如果没有请参考其它文献在自己得到local建立一个虚拟机去进行测试。 有了虚拟机之后&#xff0c;你还需要安装jdk和配置环境变量 1. 安装JDK&#xff08;以OpenJDK 17为例&#xff09; 下载JDK…

智能园区管理系统助力企业安全与效率双提升的成功案例分析

内容概要 在当今迅速发展的商业环境中&#xff0c;企业面临着资产管理、风险控制和运营效率提高等多重挑战。为了应对这些挑战&#xff0c;智能园区管理系统应运而生&#xff0c;为企业提供了全新的解决方案。例如&#xff0c;快鲸智慧园区&#xff08;楼宇&#xff09;管理系…

nacos 配置管理、 配置热更新、 动态路由

文章目录 配置管理引入jar包添加 bootstrap.yaml 文件配置在application.yaml 中添加自定义信息nacos 配置信息 配置热更新采用第一种配置根据服务名确定配置文件根据后缀确定配置文件 动态路由DynamicRouteLoaderNacosConfigManagerRouteDefinitionWriter 路由配置 配置管理 …

Linux-CentOS的yum源

1、什么是yum yum是CentOS的软件仓库管理工具。 2、yum的仓库 2.1、yum的远程仓库源 2.1.1、国内仓库 国内较知名的网络源(aliyun源&#xff0c;163源&#xff0c;sohu源&#xff0c;知名大学开源镜像等) 阿里源:https://opsx.alibaba.com/mirror 网易源:http://mirrors.1…