C++并发编程指南08

news/2025/1/31 19:39:13 标签: c++, 算法, 开发语言

以下是经过优化排版后的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变为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_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: 写入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_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: 写入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_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);  // 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_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);  // 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 函数使用释放栅栏确保 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 在栅栏前存储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 函数使用释放栅栏确保 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 在栅栏前存储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 函数使用释放栅栏确保 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.niftyadmin.cn/n/5838849.html

相关文章

基础位运算

一.基础位运算 1.<< 左移操作符&#xff0c;使二进制向左边移动指定位数&#xff0c;同时在其右侧补0 00000000 00000000 00000000 00000001 // 1的二进制 1 << 2 00000000 00000000 00000000 00000100 4 左移操作符可以用来提高乘法的效率&#xff1a;2*n ->…

【异或和之差——Trie,DP】

题目 代码 #include <bits/stdc.h> using namespace std; const int N 2e5 10; const int inf 0x3f3f3f3f; int lmax[N], lmin[N], rmax[N], rmin[N]; int ltr[1 << 22][3], rtr[1 << 22][3], idx; int n, a[N], la[N], ra[N]; void add(int x, int tr[]…

深度学习指标可视化案例

TensorBoard 代码案例&#xff1a;from torch.utils.tensorboard import SummaryWriter import torch import torchvision from torchvision import datasets, transforms# 设置TensorBoard日志路径 writer SummaryWriter(runs/mnist)# 加载数据集 transform transforms.Comp…

C28.【C++ Cont】顺序表的实现

&#x1f9e8;&#x1f9e8;&#x1f9e8;&#x1f9e8;&#x1f9e8;&#x1f9e8;&#x1f9e8;&#x1f9e8;&#x1f9e8;初二篇&#x1f9e8;&#x1f9e8;&#x1f9e8;&#x1f9e8;&#x1f9e8;&#x1f9e8;&#x1f9e8;&#x1f9e8;&#x1f9e8; 目录 1.知识回顾…

Golang 并发机制-2:Golang Goroutine 和竞争条件

在今天的软件开发中&#xff0c;我们正在使用并发的概念&#xff0c;它允许一次执行多个任务。在Go编程中&#xff0c;理解Go例程是至关重要的。本文试图详细解释什么是例程&#xff0c;它们有多轻&#xff0c;通过简单地使用“go”关键字创建它们&#xff0c;以及可能出现的竞…

【Web开发】一步一步详细分析使用Bolt.new生成的简单的VUE项目

https://bolt.new/ 这是一个bolt.new生成的Vue小项目&#xff0c;让我们来一步一步了解其架构&#xff0c;学习Vue开发&#xff0c;并美化它。 框架: Vue 3: 用于构建用户界面。 TypeScript: 提供类型安全和更好的开发体验。 Vite: 用于快速构建和开发 主界面如下&#xff1a…

图论——spfa判负环

负环 图 G G G中存在一个回路&#xff0c;该回路边权之和为负数&#xff0c;称之为负环。 spfa求负环 方法1:统计每个点入队次数, 如果某个点入队n次, 说明存在负环。 证明&#xff1a;一个点入队n次&#xff0c;即被更新了n次。一个点每次被更新时所对应最短路的边数一定是…

Android Studio 正式版 10 周年回顾,承载 Androider 的峥嵘十年

Android Studio 1.0 宣发于 2014 年 12 月&#xff0c;而现在时间来到 2025 &#xff0c;不知不觉间 Android Studio 已经陪伴 Androider 走过十年历程。 Android Studio 10 周年&#xff0c;也代表着了我的职业生涯也超十年&#xff0c;现在回想起来依然觉得「唏嘘」&#xff…