并发与系统级编程(Concurrency & Systems)
这一块是 C++ 在系统编程中的重要能力,回答的问题是:
如何在多核环境下安全、高效地让多个执行流(线程)协同工作?
可以从几层来理解:
- 线程:如何启动/管理执行流(
std::thread) - 同步原语:如何保护共享数据(
mutex/lock_guard/scoped_lock) - 原子操作:无锁地修改简单共享状态(
std::atomic) - 内存模型:不同线程之间“看见”写入的规则(memory ordering / acquire / release)
- 无锁结构:在高并发场景下减少锁的使用(lock-free programming,入门概念)
一、线程:启动与管理执行流(std::thread)
线程是“执行流”的基本单位。C++11 提供了标准的 std::thread:
#include <thread>
#include <iostream>
void worker(int id) {
std::cout << "Worker " << id << " running\n";
}
int main() {
std::thread t1(worker, 1); // 启动一个新线程
std::thread t2(worker, 2); // 再启动一个
// 等待两个线程结束
t1.join();
t2.join();
}
要点:
- 构造
std::thread时,就启动了一个新线程,执行给定的函数/可调用对象; join():等待线程结束(阻塞当前线程);detach():让线程在后台运行,主线程不再等待它(小心资源与生命周期)。
常见坑:
- 如果线程对象在析构前既没
join()也没detach(),程序会std::terminate(); - 所以要么在作用域结束前
join(),要么明确detach(),不要“忘记管它”。
二、锁与互斥量:保护共享数据(mutex / lock_guard / scoped_lock)
当多个线程访问同一份可写数据时,需要保证:
在任意时刻,只有一个线程可以修改这块数据。
这就用到互斥量(std::mutex)和各种锁封装。
2.1 std::mutex 与手动加锁/解锁
#include <mutex>
#include <thread>
#include <vector>
int counter = 0;
std::mutex m;
void increment() {
for (int i = 0; i < 100000; ++i) {
m.lock();
++counter;
m.unlock();
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
}
问题:lock() / unlock() 手写非常容易忘记在异常路径或早返回路径解锁。
2.2 std::lock_guard:RAII 封装锁
std::lock_guard 利用 RAII,在构造时加锁,在析构时自动解锁:
void increment_safe() {
for (int i = 0; i < 100000; ++i) {
std::lock_guard<std::mutex> lk(m); // 构造时 lock
++counter;
} // 作用域结束自动 unlock
}
只要写成这种“局部对象 + 作用域”的形式,即使中途 return 或抛异常,锁也会自动释放。
2.3 std::scoped_lock:一次锁多个互斥量(避免死锁)
当需要同时锁多个 mutex 时,容易造成死锁。std::scoped_lock 可以一次性安全锁多个:
std::mutex m1, m2;
void work() {
std::scoped_lock lk(m1, m2); // 一次性锁定 m1 和 m2,避免死锁
// 在这里安全访问受 m1/m2 保护的资源
}
它内部使用无死锁的加锁策略(类似 std::lock)。
三、原子操作:简单共享状态的无锁修改(std::atomic)
对于一些简单类型(如整数、指针、布尔值),可以用 std::atomic 在 不使用互斥量 的情况下安全读写:
#include <atomic>
std::atomic<int> acounter{0};
void increment_atomic() {
for (int i = 0; i < 100000; ++i) {
acounter.fetch_add(1, std::memory_order_relaxed);
// 或者 acounter++;
}
}
特点:
- 对单个
std::atomic<T>的读写是原子的(不会被其他线程“撕裂”); - 对简单计数器、标志位、引用计数等非常适合;
- 但原子并不自动解决所有同步问题——需要结合内存模型来理解“其他线程什么时候能看到这个写入”。
std::atomic 的常用成员:
load()/store()exchange()fetch_add()/fetch_sub()/fetch_or()/ ...
四、内存模型与内存序(Memory Model & Memory Ordering)
在多线程环境下,光做到“单个变量的修改是原子的”还不够,还要回答:
一个线程的写入,另一个线程何时、以什么顺序可见?
C++ 内存模型通过 memory ordering 来描述这点,常见枚举值有:
std::memory_order_relaxedstd::memory_order_acquirestd::memory_order_releasestd::memory_order_acq_relstd::memory_order_seq_cst(默认,最强保证)
4.1 acquire / release 模式的直观理解
最常用的模式是 release + acquire,用于构建“安全发布(publish)”:
std::atomic<bool> ready{false};
int data = 0;
void producer() {
data = 42; // 1. 写入共享数据
ready.store(true, std::memory_order_release); // 2. 发布信号
}
void consumer() {
while (!ready.load(std::memory_order_acquire)) {
// 自旋等待
}
// 一旦 load(acquire) 看到 ready 为 true,
// 就保证能看到 1 中对 data 的写入
int x = data; // 安全读取
}
语义可以粗略理解为:
release:之前对内存的写入,在这个 release 操作前面,对其他 acquire 线程可见;acquire:之后对内存的读写,在这个 acquire 操作后面,不会“跑到前面去”。
比喻:
ready.store(..., release)像是“门卫放行前,先把屋子整理好”;ready.load(..., acquire)像是“进入屋子时,保证看到的是整理好的状态”。
4.2 relaxed 与 seq_cst
relaxed:只保证原子性,不保证任何顺序与可见性关系;seq_cst:最强保证,逻辑上像所有原子操作都按某个全局顺序执行(默认内存序)。
实践中:
- 初学/一般业务:可以先用默认的
seq_cst; - 性能敏感 + 有足够经验时,再精细用
acquire/release/relaxed。
五、无锁结构与 lock-free 编程(Lock-Free Programming)
在高并发、低延迟系统(如撮合引擎、网络服务)中,互斥锁有时会成为性能瓶颈:
- 锁竞争导致上下文切换;
- 阻塞导致延迟抖动;
无锁结构(lock-free) 试图通过原子操作 + 内存模型构建数据结构,不使用传统互斥锁:
- lock-free 队列(MPSC/SPMC/MPMC)
- lock-free 栈
- 原子引用计数(如
shared_ptr的内部实现)
一个非常常见的基础原语是 CAS(Compare-And-Swap),在 C++ 中对应:
std::atomic::compare_exchange_strongstd::atomic::compare_exchange_weak
示意代码(不建议直接用于生产,仅展示模式):
std::atomic<int> value{0};
void try_update(int expected, int desired) {
value.compare_exchange_strong(
expected, // 预期旧值
desired, // 想要写入的新值
std::memory_order_acq_rel,
std::memory_order_relaxed
);
}
要点:
- 如果
value当前等于expected,就写入desired,并返回true; - 否则不修改
value,并把当前值写回expected,返回false; - 通过循环 + CAS,可以在不加锁的情况下实现“乐观并发修改”。
真正的 lock-free 数据结构设计对:
- ABA 问题
- 内存回收(hazard pointers / epoch-based reclamation)
- 内存序的精确控制
要求都很高,一般是库作者、基础设施团队才会深入实现。日常业务更多是理解其存在和大致原理,选用成熟库。
六、整体小结:从线程到无锁
可以把 C++ 并发与系统编程这一块理解为一条从“粗粒度到细粒度”的能力梯度:
- 线程:
std::thread启动/管理执行流 - 互斥量与锁封装:
std::mutex+lock_guard/scoped_lock保护共享数据 - 原子操作:
std::atomic安全修改简单共享状态 - 内存模型:memory ordering(尤其是 acquire/release)定义跨线程可见性
- 无锁结构:在理解原子与内存模型的基础上,构建高性能 lock-free 数据结构
掌握前 3 层 + 对内存模型的直观理解,已经足以应对大部分 C++ 并发场景;
真正深入无锁编程,则更多是系统编程和底层库开发的主题。