最稳妥做法是用 std::lock_guard 或 std::unique_lock 封装 std::mutex:前者适用于简单临界区,构造加锁、析构解锁、不可复制移动;后者支持延迟加锁等灵活操作。

用 std::mutex + RAII 封装最稳妥
直接暴露 std::mutex 和手写 lock()/unlock() 是典型反模式——容易忘解锁、异常时泄漏锁、逻辑分散。C++ 标准库早提供了 std::unique_lock 和 std::lock_guard,它们本质就是 RAII 封装:构造即加锁,析构即释放,不管是否异常都安全。
实际封装时别自己重造轮子,优先组合标准组件:
-
std::lock_guard<:mutex></:mutex>适合简单临界区:构造时阻塞加锁,离开作用域自动释放,不可转移、不可复制,开销最小 -
std::unique_lock<:mutex></:mutex>更灵活:支持延迟加锁、手动解锁、条件变量配合、可移动,但有轻微额外开销 - 不要在类里裸存
std::mutex并提供lock()成员函数——这等于把锁管理责任又甩回用户手上
成员函数内部加锁比暴露锁更安全
常见错误是把 std::mutex 声明为 public 成员,让用户自己调用 lock()。这破坏了封装,也违背“谁拥有资源,谁管理生命周期”的原则。
正确做法是把锁设为 private,并在每个需要同步的成员函数内部完成加锁/解锁:
class ThreadSafeCounter {
private:
mutable std::mutex mtx_; // mutable 允许 const 成员函数中加锁
int value_ = 0;
public:
void increment() {
std::lock_guard<:mutex> lock(mtx_);
++value_;
}
int get() const {
std::lock_guard<:mutex> lock(mtx_);
return value_;
}
};</:mutex></:mutex>
注意 mutable 修饰 mtx_:否则 const 成员函数(如 get())无法调用 lock() ——这是容易忽略的编译错误点。
避免递归锁,除非真需要
std::recursive_mutex 允许同一线程多次加锁,看似“更安全”,实则掩盖设计问题。绝大多数场景下,递归锁意味着函数边界不清、职责混杂,或无意中形成了嵌套调用。
如果确实遇到必须递归加锁的逻辑(比如模板回调触发自身),才考虑它;否则坚持用 std::mutex,让编译器/运行时报错帮你发现死锁苗头:
- Windows 上可能抛
std::system_error(错误码resource_deadlock_would_occur) - Linux 下行为未定义,通常卡死或崩溃
- 用
std::mutex而非std::recursive_mutex是主动防御
性能敏感时慎用细粒度锁
有人以为“每个成员变量配一个 std::mutex”就是最佳实践,结果锁竞争没减少,反而缓存行失效(false sharing)加剧。现代 CPU 缓存行通常是 64 字节,相邻小变量即使各自加锁,也可能落在同一缓存行里。
优化方向更可能是:
- 合并多个相关字段到一个结构体,用单个锁保护整块数据
- 用读写锁(
std::shared_mutex)区分读多写少场景 - 评估无锁方案(如
std::atomic)是否适用——但别过早优化,先测瓶颈
真正难的不是怎么加锁,而是判断哪些操作必须原子、哪些可以放宽一致性要求。这点没法靠封装解决,得回到业务逻辑本身去权衡。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











