从 C++ 基础到掌握 — 第 20 集
使用线程、atomic 和内存模型处理并发
本博客文章以教学为目的编写。 内容可能存在错误,请结合目标编译器和标准库文档进行确认。
多个线程访问同一内存时,问题不只是执行顺序不固定。按照 C++ memory model,未同步的 data race 是 undefined behavior。
明确保护共享不变量
class Inventory
{
public:
void Add(Item item)
{
std::lock_guard lock(Mutex);
Items.push_back(std::move(item));
}
private:
std::mutex Mutex;
std::vector<Item> Items;
};
std::lock_guard 在离开作用域时释放 mutex,包括异常路径。锁应保护完整不变量,而不只是一次 vector 调用。
以不同顺序获取多个 mutex 可能 deadlock。可以用 std::scoped_lock 一起锁定,或制定项目统一的加锁顺序。
Atomic 适合小型独立值
std::atomic<unsigned int> completedJobs{0};
void MarkCompleted()
{
completedJobs.fetch_add(1, std::memory_order_relaxed);
}
不负责发布其他状态的统计计数器可能只需 relaxed ordering。但一个 atomic 计数器不会自动保护多字段对象的一致性。
默认 sequential consistency 更容易推理。acquire、release、relaxed 等较弱顺序应建立在已证明的 happens-before 关系上,而不是性能猜测。
用 jthread 绑定结束生命周期
void Worker(std::stop_token stop)
{
while (!stop.stop_requested())
ProcessOneJob();
}
void Run()
{
std::jthread worker(Worker);
// 析构时请求停止并 join。
}
C++20 std::jthread 在析构时请求停止并等待线程。stop_token 是协作式取消;无法响应的 blocking I/O 仍需 timeout 或唤醒机制。
更好的第一步是移除共享修改状态。给 worker 独立输入,再通过消息或 queue 收集结果。不同 atomic 若位于同一 cache line,也可能因 false sharing 变慢。
一次测试成功不能证明并发代码安全。应记录所有权、结束条件、锁顺序和数据发布规则,并用 ThreadSanitizer 与可复现压力测试验证。