必须用std::unique_lock的场景:延迟加锁、提前解锁、所有权转移、配合condition_variable;仅需作用域内自动加/解锁时,应优先用更轻量安全的std::lock_guard。

std::unique_lock 什么时候必须用,什么时候可以不用
直接说结论:std::unique_lock 不是 std::lock_guard 的“升级版”,而是“功能更细、代价更高”的替代方案。如果你只是进作用域加锁、出作用域解锁,用 std::lock_guard 更轻量、更安全;只有当你需要手动控制锁的生命周期(比如延迟加锁、提前解锁、转移所有权、配合条件变量),才必须用 std::unique_lock。
常见误用是“听说它更灵活,就全换成它”——这会带来额外的成员变量开销(内部存一个 mutex* 和一个是否已加锁的状态位),且编译器更难优化。
怎么正确构造和转移 std::unique_lock
std::unique_lock 支持多种构造方式,但不是所有都安全。关键点在于:它默认是可移动(move-only)的,不可拷贝;且默认构造后不持有锁。
-
std::unique_lock<:mutex> lk{mtx}</:mutex>:立即尝试加锁,失败则抛std::system_error -
std::unique_lock<:mutex> lk{mtx, std::defer_lock}</:mutex>:构造但不加锁,后续调用lk.lock()或lk.try_lock() -
std::unique_lock<:mutex> lk{mtx, std::try_to_lock}</:mutex>:非阻塞尝试加锁,失败时lk.owns_lock() == false -
std::unique_lock<:mutex> lk2 = std::move(lk1)</:mutex>:合法,lk1变为未持有锁状态;但lk1 = lk2编译失败(无拷贝赋值)
为什么 std::condition_variable::wait 必须配 std::unique_lock
因为 std::condition_variable::wait 需要在等待前原子地释放锁,并在唤醒后重新加锁。这个“释放+挂起+重获”动作必须由锁对象自己完成,而只有 std::unique_lock 提供了 unlock() 和 lock() 接口,且支持临时放弃所有权。
立即学习“C++免费学习笔记(深入)”;
错误写法:std::lock_guard 传给 wait 会编译失败;用 std::unique_lock 但忘了传引用(比如写成值传递)会导致锁被移动走,线程在 wait 返回后访问已失效的锁对象。
正确示例:
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
// ...
std::unique_lock<std::mutex> lk{mtx};
cv.wait(lk, []{ return !data_queue.empty(); }); // 注意:lk 是引用传入
int val = data_queue.front();
data_queue.pop();
// lk 自动在 wait 返回后重新加锁,离开作用域才释放
容易忽略的坑:临时解锁后忘记重新加锁,或跨作用域误用
std::unique_lock 的 unlock() 和 lock() 是公开接口,但手动调用极易出错。典型问题包括:
- 调用
lk.unlock()后,在同一线程里又调用lk.lock()—— 看似合理,但若此时锁被其他线程抢走,当前线程会阻塞;而你本意可能是“短暂放开锁做耗时操作”,结果反而卡住 - 把
std::unique_lock对象返回出去(比如函数返回值),依赖 move 语义没问题;但如果在函数内return lk;前已经lk.unlock(),那返回的是一个空锁,接收方可能误以为它还管着资源 - 在 lambda 捕获中按值捕获
std::unique_lock(比如[lk](){...})—— 这会触发 move,原作用域的锁立刻失效,极难调试
真正需要手动干预锁时,优先考虑封装成 RAII 辅助类,而不是裸露调用 unlock/lock。多数场景下,“延迟构造 + 自动析构”已经足够,强行拆解反而增加不确定性。


















