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

发布于:2025-09-05 ⋅ 阅读:(14) ⋅ 点赞:(0)

5.3 同步操作与强制排序

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

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

#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 - 顺序一致性序(默认)

顺序一致性序

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

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

自由序

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

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

获取-释放序

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

#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 释放序列与同步

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

#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 栅栏

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

#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 原子操作对非原子操作排序

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

#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 非原子操作排序

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

#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等,它们都基于这些基本的内存序概念构建,为多线程编程提供了更高级的抽象。

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


网站公告

今日签到

点亮在社区的每一天
去签到