C++中的内存模型与并发:从原子到内存序

在 C++17 及以后的标准中,内存模型(Memory Model)为并发程序提供了严格的语义定义。它定义了多线程环境下对象访问的可见性、顺序性以及相关的同步机制。本文将从内存模型的基本概念、原子类型、内存序(memory order)以及常用同步原语四个层面展开,帮助你在实际编程中正确、高效地使用并发。


一、内存模型的基本概念

C++内存模型的核心是内存序(memory order)数据竞争(data race)。任何对同一共享内存的并发读写,如果至少有一次写操作且未被同步约束,就会产生数据竞争,导致程序行为未定义。

内存模型提供了两类基本规则:

  1. 顺序一致性(sequential consistency):若所有操作遵循顺序一致性,所有线程会看到全局一致的操作顺序。
  2. 同步约束(synchronization order):保证同一线程中的顺序以及不同线程通过同步原语建立的可见性。

通过标准提供的原子操作与内存序,我们可以在不使用传统互斥锁的情况下实现高效并发。


二、原子类型(std::atomic

std::atomic 模板是 C++ 并发编程的核心。它封装了可以安全并发访问的基本类型(如 int, bool, void* 等),并提供了一套成员函数支持读写、原子比较交换等操作。

1. 基本使用

#include <atomic>
#include <thread>
#include <iostream>

std::atomic <int> counter{0};

void worker() {
    for (int i = 0; i < 1000; ++i) {
        counter.fetch_add(1, std::memory_order_relaxed);
    }
}

int main() {
    std::thread t1(worker), t2(worker);
    t1.join(); t2.join();
    std::cout << counter << '\n'; // 2000
}

2. 原子类型的细节

  • load()store():分别用于读取和写入,支持显式内存序参数。
  • fetch_add()fetch_sub():返回旧值的算术原子操作。
  • compare_exchange_weak() / compare_exchange_strong():原子比较并交换,常用于实现无锁算法。

三、内存序(Memory Order)

C++ 为原子操作提供了多种内存序,允许开发者在性能与可见性之间做权衡。常见的内存序有:

序号 名称 含义
0 memory_order_relaxed 仅保证原子性,不保证任何同步。
1 memory_order_consume 只保证数据依赖的可见性,常被忽略。
2 memory_order_acquire 读操作之前的所有读写对后续读写可见。
3 memory_order_release 写操作之后的所有读写对前面读写可见。
4 memory_order_acq_rel 组合 acquire 和 release。
5 memory_order_seq_cst 全序一致性,默认顺序。

1. 典型场景

  • 生产者-消费者:生产者 store(..., memory_order_release),消费者 load(..., memory_order_acquire)
  • 无锁队列:使用 acq_relseq_cst 确保操作完整性。

2. 性能考量

  • relaxed 是最快的,但只能在不需要可见性的地方使用。
  • seq_cst 提供最强的保证,但会引入更大的同步成本。

四、常用同步原语

1. std::mutexstd::lock_guard

最直观的互斥锁实现:

#include <mutex>
std::mutex mtx;
int shared = 0;

void safe_increment() {
    std::lock_guard<std::mutex> lock(mtx);
    ++shared;
}

2. std::shared_mutex

读写锁,允许多个读线程并行,写线程互斥:

#include <shared_mutex>
std::shared_mutex rw_mtx;

void reader() {
    std::shared_lock<std::shared_mutex> lock(rw_mtx);
    // 读取共享资源
}

void writer() {
    std::unique_lock<std::shared_mutex> lock(rw_mtx);
    // 写入共享资源
}

3. std::condition_variable

用于线程间的等待与通知:

#include <condition_variable>
std::mutex cv_mtx;
std::condition_variable cv;
bool ready = false;

void waiter() {
    std::unique_lock<std::mutex> lock(cv_mtx);
    cv.wait(lock, []{ return ready; });
    // 继续执行
}

五、无锁算法实例:CAS(Compare-And-Swap)

CAS 是实现无锁数据结构的核心原语。以下是一个简单的单线程单消费者(SPSC)队列实现示例:

#include <atomic>
#include <vector>

template <typename T>
class SPSCQueue {
public:
    explicit SPSCQueue(size_t size) : buf(size), mask(size - 1) {}

    bool push(const T& item) {
        size_t pos = tail.load(std::memory_order_relaxed);
        if ((pos - head.load(std::memory_order_acquire)) == buf.size())
            return false; // full
        buf[pos & mask] = item;
        tail.store(pos + 1, std::memory_order_release);
        return true;
    }

    bool pop(T& item) {
        size_t pos = head.load(std::memory_order_relaxed);
        if (pos == tail.load(std::memory_order_acquire))
            return false; // empty
        item = buf[pos & mask];
        head.store(pos + 1, std::memory_order_release);
        return true;
    }

private:
    std::vector <T> buf;
    size_t mask;
    std::atomic <size_t> head{0};
    std::atomic <size_t> tail{0};
};

六、常见陷阱与最佳实践

  1. 数据竞争导致未定义行为
    任何未同步的并发读写都是数据竞争。始终使用 std::atomic 或互斥锁包裹共享变量。

  2. 错误的内存序使用
    acquirerelease 必须匹配;relaxed 只能在不需要可见性的场景使用。

  3. 无锁结构的正确性
    设计无锁数据结构时,需考虑ABA问题(即值先变为 B,再回到 A)。可使用版本号或 std::atomic<std::uintptr_t> 组合。

  4. 性能测试
    并发程序的性能与 CPU 缓存、内存模型紧密相关。使用 std::atomicmemory_order_relaxed 可以大幅提升吞吐量,但必须保证逻辑正确。


结语

C++ 内存模型为并发编程提供了强大而灵活的工具。通过理解原子类型、内存序以及同步原语的细节,你可以在保证程序正确性的前提下,最大化多核系统的利用率。希望本文能帮助你在日常编码中更自如地运用这些技术,写出既高效又安全的并发代码。

发表评论