在 C++11 之后,标准为并发编程提供了完整的内存模型。了解这一模型对于编写可移植、线程安全的代码至关重要。本文将从内存模型的核心概念、同步原语的实现以及实际使用场景三方面进行阐述。
1. 内存模型的基本概念
1.1 线程、操作和操作序
- 线程:执行顺序的独立流。
- 操作:对共享变量的读、写、原子操作。
- 操作序:程序执行过程中操作的天然顺序。
1.2 观察序(happens‑before)
- happens‑before 关系规定了操作的可见性:如果操作
Ahappens‑before 操作B,则A的副作用对B可见。 - 通过 同步原语(如
std::mutex、std::atomic)显式建立该关系。
1.3 原子操作与顺序性
- 原子类型(`std::atomic `)保证单个操作不可被打断。
- 原子操作有不同的 memory order:
memory_order_seq_cst(默认,顺序一致)memory_order_relaxed(不保证顺序)memory_order_acquire/memory_order_release(建立 acquire/release 关系)memory_order_acq_rel、memory_order_consume(较少使用)
2. 同步原语的实现细节
2.1 std::mutex 与锁
- 基于操作系统的互斥量实现。
std::lock_guard、std::unique_lock提供 RAII 方式获取/释放锁。- 锁的粒度决定性能:过宽锁导致竞争,过窄锁导致错误。
2.2 原子变量与无锁编程
- 通过
std::atomic实现无锁数据结构(如无锁队列、无锁链表)。 - 必须严格遵守 ABA 问题,通常使用
std::atomic<std::shared_ptr<T>>或带版本号的指针包装。
2.3 条件变量与等待
std::condition_variable结合std::unique_lock实现线程同步等待。- 必须在等待前检查条件,以防止假唤醒。
2.4 线程局部存储(TLS)
thread_local关键字保证每个线程都有独立实例,避免共享竞争。
3. 典型场景与最佳实践
3.1 生产者-消费者
std::queue <int> q;
std::mutex m;
std::condition_variable cv;
bool finished = false;
void producer() {
for(int i=0;i<100;i++){
{
std::lock_guard<std::mutex> lk(m);
q.push(i);
}
cv.notify_one();
}
{
std::lock_guard<std::mutex> lk(m);
finished = true;
}
cv.notify_all();
}
void consumer() {
while(true){
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{ return !q.empty() || finished; });
while(!q.empty()){
int v = q.front(); q.pop();
lk.unlock();
process(v);
lk.lock();
}
if(finished) break;
}
}
- 通过
cv.wait的 谓语 防止假唤醒。 - 使用
lock_guard或unique_lock控制锁的生命周期。
3.2 延迟初始化(双重检查锁)
class Singleton {
static std::atomic<Singleton*> instance;
public:
static Singleton* get() {
Singleton* tmp = instance.load(std::memory_order_acquire);
if (!tmp) {
std::lock_guard<std::mutex> lk(m);
tmp = instance.load(std::memory_order_relaxed);
if (!tmp) {
tmp = new Singleton();
instance.store(tmp, std::memory_order_release);
}
}
return tmp;
}
private:
Singleton() {}
static std::mutex m;
};
- 通过
memory_order_acquire/release确保对象构造完成后可见。
3.3 原子计数器
std::atomic <int> counter{0};
void worker() {
for(int i=0;i<1000;i++)
counter.fetch_add(1, std::memory_order_relaxed);
}
- 对计数器使用
memory_order_relaxed即可,因为仅需要原子性,不涉及其他可见性。
4. 性能调优建议
- 优先使用原子:在可能的情况下,使用无锁原子操作减少锁开销。
- 避免过度锁定:尽量缩小临界区,仅保护真正需要同步的代码。
- 利用缓存行对齐:对频繁访问的共享数据使用
alignas(64)避免伪共享。 - 合理使用
memory_order_relaxed:当只需要原子性时,使用 relaxed 顺序可提高性能。 - 测量而非假设:使用工具(如
perf、Valgrind)验证锁竞争和 CPU 缓存失效情况。
5. 小结
C++ 的内存模型为并发编程提供了强大的语义保障,但要充分发挥其优势,需要深入理解 happens‑before 关系、原子类型 的内存顺序,以及 同步原语 的正确使用。通过合理选择锁、原子与条件变量,并结合性能调优技巧,能够在保证线程安全的前提下实现高效的多线程程序。