在 C++17 及以后的标准中,内存模型(Memory Model)为并发程序提供了严格的语义定义。它定义了多线程环境下对象访问的可见性、顺序性以及相关的同步机制。本文将从内存模型的基本概念、原子类型、内存序(memory order)以及常用同步原语四个层面展开,帮助你在实际编程中正确、高效地使用并发。
一、内存模型的基本概念
C++内存模型的核心是内存序(memory order)和数据竞争(data race)。任何对同一共享内存的并发读写,如果至少有一次写操作且未被同步约束,就会产生数据竞争,导致程序行为未定义。
内存模型提供了两类基本规则:
- 顺序一致性(sequential consistency):若所有操作遵循顺序一致性,所有线程会看到全局一致的操作顺序。
- 同步约束(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_rel或seq_cst确保操作完整性。
2. 性能考量
relaxed是最快的,但只能在不需要可见性的地方使用。seq_cst提供最强的保证,但会引入更大的同步成本。
四、常用同步原语
1. std::mutex 与 std::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};
};
六、常见陷阱与最佳实践
-
数据竞争导致未定义行为
任何未同步的并发读写都是数据竞争。始终使用std::atomic或互斥锁包裹共享变量。 -
错误的内存序使用
acquire与release必须匹配;relaxed只能在不需要可见性的场景使用。 -
无锁结构的正确性
设计无锁数据结构时,需考虑ABA问题(即值先变为 B,再回到 A)。可使用版本号或std::atomic<std::uintptr_t>组合。 -
性能测试
并发程序的性能与 CPU 缓存、内存模型紧密相关。使用std::atomic的memory_order_relaxed可以大幅提升吞吐量,但必须保证逻辑正确。
结语
C++ 内存模型为并发编程提供了强大而灵活的工具。通过理解原子类型、内存序以及同步原语的细节,你可以在保证程序正确性的前提下,最大化多核系统的利用率。希望本文能帮助你在日常编码中更自如地运用这些技术,写出既高效又安全的并发代码。