并发编程指南 同步操作与强制排序

文章目录

  • [5.3 同步操作与强制排序](#5.3 同步操作与强制排序)
    • [代码5.2 多线程数据读写示例](#代码5.2 多线程数据读写示例)
    • [5.3.1 同步发生](#5.3.1 同步发生)
    • [5.3.2 先行发生](#5.3.2 先行发生)
    • [5.3.3 原子操作的内存序](#5.3.3 原子操作的内存序)
    • [5.3.4 释放序列与同步](#5.3.4 释放序列与同步)
    • [5.3.5 栅栏](#5.3.5 栅栏)
    • [5.3.6 原子操作对非原子操作排序](#5.3.6 原子操作对非原子操作排序)
    • [5.3.7 非原子操作排序](#5.3.7 非原子操作排序)

5.3 同步操作与强制排序

在多线程编程中,当多个线程同时访问共享数据时,需要谨慎处理同步问题。让我们通过一个简单例子来理解这个概念:假设一个线程向数据结构写入数据,另一个线程从中读取数据。为了避免数据竞争,写入线程会设置一个标志位表示数据已准备就绪,读取线程则需等待该标志位被设置后才能读取数据。

代码5.2 多线程数据读写示例

cpp 复制代码
#include <vector>
#include <atomic>
#include <iostream>
#include <thread>

std::vector<int> data;
std::atomic<bool> data_ready(false);

void reader_thread()
{
  while(!data_ready.load())  // 1. 等待数据准备就绪
  {
    std::this_thread::sleep(std::chrono::milliseconds(1));
  }
  std::cout << "The answer=" << data[0] << "\n";  // 2. 读取数据
}

void writer_thread()
{
  data.push_back(42);  // 3. 写入数据
  data_ready = true;   // 4. 设置数据就绪标志
}

int main()
{
  std::thread writer(writer_thread);
  std::thread reader(reader_thread);
  
  writer.join();
  reader.join();
  
  return 0;
}

在这个例子中,虽然等待循环①本身是原子的,但非原子读取操作②和写入操作③如果无序执行,就会产生未定义行为。我们通过原子变量data_ready的操作来建立执行顺序:数据写入③必须先于标志设置④,标志检查①必须先于数据读取②。当data_ready为true时,写操作与读操作同步,建立了"先行"关系。

5.3.1 同步发生

"同步发生"关系只在原子类型操作间存在。当线程A执行原子写操作,线程B执行原子读操作且读取的是A写入的值(或之后写入的值),那么A的写操作与B的读操作就是同步发生关系。

5.3.2 先行发生

"先行发生"关系是程序操作顺序的基本构建块。在单线程中,如果操作A在操作B之前执行,那么A就先行于B。在多线程环境中,如果操作A与另一线程上的操作B同步发生,那么A线程间先行于B。

5.3.3 原子操作的内存序

C++提供了六种内存序选项:

  1. memory_order_relaxed - 自由序
  2. memory_order_consume - 消费序(C++17中不推荐使用)
  3. memory_order_acquire - 获取序
  4. memory_order_release - 释放序
  5. memory_order_acq_rel - 获取-释放序
  6. memory_order_seq_cst - 顺序一致性序(默认)

顺序一致性序

顺序一致性是最严格的内存序,保证所有线程看到的操作顺序一致。下面是顺序一致性的示例:

cpp 复制代码
#include <atomic>
#include <thread>
#include <assert.h>

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;
}

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); // 永远不会触发
}

自由序

自由序只保证原子操作的原子性,不提供任何顺序保证:

cpp 复制代码
#include <atomic>
#include <thread>
#include <assert.h>

std::atomic<bool> x, y;
std::atomic<int> z;

void write_x_then_y()
{
  x.store(true, std::memory_order_relaxed);  // 1
  y.store(true, std::memory_order_relaxed);  // 2
}

void read_y_then_x()
{
  while(!y.load(std::memory_order_relaxed));  // 3
  if(x.load(std::memory_order_relaxed)) ++z;  // 4
}

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); // 可能触发!
}

获取-释放序

获取-释放序提供了比自由序更强的同步保证:

cpp 复制代码
#include <atomic>
#include <thread>
#include <assert.h>

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); // 不会触发
}

5.3.4 释放序列与同步

释放序列确保了一系列原子操作的正确同步:

cpp 复制代码
#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);
}

void process(int item) { /* 处理数据 */ }

void consume_queue_items()
{
  while(true)
  {
    int item_index;
    if((item_index = count.fetch_sub(1, std::memory_order_acquire)) <= 0)
    {
      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();
}

5.3.5 栅栏

内存栅栏提供了对内存操作顺序的强制约束:

cpp 复制代码
#include <atomic>
#include <thread>
#include <assert.h>

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); // 不会触发
}

5.3.6 原子操作对非原子操作排序

原子操作也可以对非原子操作进行排序:

cpp 复制代码
#include <atomic>
#include <thread>
#include <assert.h>

bool x = false; // 非原子变量
std::atomic<bool> y;
std::atomic<int> z;

void write_x_then_y()
{
  x = true; // 非原子写入
  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) ++z; // 读取非原子变量
}

int main()
{
  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 非原子操作排序

非原子操作可以通过原子操作进行排序,这是更高级同步工具的基础:

cpp 复制代码
#include <atomic>
#include <thread>

class spinlock_mutex
{
  std::atomic_flag flag;
public:
  spinlock_mutex() : flag(ATOMIC_FLAG_INIT) {}
  
  void lock()
  {
    while(flag.test_and_set(std::memory_order_acquire));
  }
  
  void unlock()
  {
    flag.clear(std::memory_order_release);
  }
};

spinlock_mutex mutex;
int shared_data = 0;

void worker()
{
  mutex.lock();
  ++shared_data; // 受保护的操作
  mutex.unlock();
}

int main()
{
  std::thread t1(worker);
  std::thread t2(worker);
  
  t1.join();
  t2.join();
  
  return 0;
}

C++标准库提供了多种同步机制,包括互斥量、条件变量、future等,它们都基于这些基本的内存序概念构建,为多线程编程提供了更高级的抽象。

理解这些内存序概念对于编写正确高效的多线程程序至关重要。在实际开发中,应该优先使用高级同步工具,只有在需要极致性能时才考虑直接使用原子操作和内存序。

相关推荐
董董灿是个攻城狮10 小时前
AI视觉连载8:传统 CV 之边缘检测
算法
blasit17 小时前
笔记:Qt C++建立子线程做一个socket TCP常连接通信
c++·qt·tcp/ip
AI软著研究员17 小时前
程序员必看:软著不是“面子工程”,是代码的“法律保险”
算法
FunnySaltyFish17 小时前
什么?Compose 把 GapBuffer 换成了 LinkBuffer?
算法·kotlin·android jetpack
颜酱18 小时前
理解二叉树最近公共祖先(LCA):从基础到变种解析
javascript·后端·算法
地平线开发者1 天前
SparseDrive 模型导出与性能优化实战
算法·自动驾驶
董董灿是个攻城狮1 天前
大模型连载2:初步认识 tokenizer 的过程
算法
地平线开发者1 天前
地平线 VP 接口工程实践(一):hbVPRoiResize 接口功能、使用约束与典型问题总结
算法·自动驾驶
罗西的思考1 天前
AI Agent框架探秘:拆解 OpenHands(10)--- Runtime
人工智能·算法·机器学习
HXhlx2 天前
CART决策树基本原理
算法·机器学习