使用unique_lock避免死锁
std::unique_lock 是 C++ 中灵活的互斥锁管理工具,其核心优势在于支持手动锁定/解锁、延迟锁定、锁所有权转移等特性。这些特性使其能有效避免死锁——尤其是通过控制锁的获取顺序、减少锁持有时间、配合条件变量自动释放锁等方式。
一、核心原理:unique_lock 如何帮助避免死锁?
死锁的典型原因是:多个线程按不同顺序获取多个锁,或线程持有锁时永久阻塞(如等待其他资源)。unique_lock 的灵活性恰好针对这些场景:
- 支持延迟锁定(创建时不立即锁定,后续手动控制),便于统一加锁顺序;
- 支持手动解锁(无需等到析构),可提前释放锁,减少持有时间;
- 配合条件变量时,
wait()会自动释放锁,避免等待时持有锁导致的死锁; - 支持移动语义(所有权转移),避免不必要的锁复制或竞争。
二、具体场景与代码示例
1. 控制加锁顺序,避免“循环等待”死锁
死锁场景:两个线程需要获取两把锁,但按相反顺序加锁(线程1先锁A再锁B,线程2先锁B再锁A),导致互相等待。
解决方法:用 unique_lock 的延迟锁定(std::defer_lock),确保所有线程按相同顺序获取锁。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtxA, mtxB; // 两把需要竞争的锁
// 错误示例:加锁顺序不一致导致死锁
void wrong_thread1() {
std::lock_guard<std::mutex> lockA(mtxA); // 先锁A
std::cout << "线程1:已获取锁A,等待锁B...\n";
std::lock_guard<std::mutex> lockB(mtxB); // 再锁B(可能死锁)
std::cout << "线程1:已获取锁B,执行操作\n";
}
void wrong_thread2() {
std::lock_guard<std::mutex> lockB(mtxB); // 先锁B(与线程1顺序相反)
std::cout << "线程2:已获取锁B,等待锁A...\n";
std::lock_guard<std::mutex> lockA(mtxA); // 再锁A(可能死锁)
std::cout << "线程2:已获取锁A,执行操作\n";
}
// 正确示例:用unique_lock延迟锁定,统一加锁顺序(先A后B)
void correct_thread1() {
// 延迟锁定:创建unique_lock但不立即锁定
std::unique_lock<std::mutex> lockA(mtxA, std::defer_lock);
std::unique_lock<std::mutex> lockB(mtxB, std::defer_lock);
// 按固定顺序加锁(先A后B)
std::lock(lockA, lockB); // 原子操作:同时获取所有锁,避免中间被打断
std::cout << "线程1:已获取锁A和B,执行操作\n";
}
void correct_thread2() {
std::unique_lock<std::mutex> lockA(mtxA, std::defer_lock);
std::unique_lock<std::mutex> lockB(mtxB, std::defer_lock);
// 与线程1保持相同顺序(先A后B)
std::lock(lockA, lockB); // 按统一顺序加锁,避免死锁
std::cout << "线程2:已获取锁A和B,执行操作\n";
}
int main() {
// 测试错误场景(可能死锁,需注释掉正确场景测试)
// std::thread t1(wrong_thread1);
// std::thread t2(wrong_thread2);
// 测试正确场景(无死锁)
std::thread t1(correct_thread1);
std::thread t2(correct_thread2);
t1.join();
t2.join();
return 0;
}
关键:std::defer_lock 让 unique_lock 初始化时不锁定,后续用 std::lock(lock1, lock2) 按固定顺序原子性获取所有锁,避免“部分加锁”导致的循环等待。
2. 提前解锁,减少锁持有时间
死锁风险:线程持有锁时执行耗时操作(如IO、sleep),导致其他线程长时间无法获取锁,可能引发死锁。
解决方法:用 unique_lock::unlock() 手动提前释放锁,仅在操作共享资源时持有锁。
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtx;
int shared_data = 0;
// 错误示例:持有锁执行耗时操作,导致其他线程阻塞
void wrong_worker() {
std::lock_guard<std::mutex> lock(mtx); // 锁会一直持有到函数结束
shared_data++; // 操作共享资源(仅需短暂持有锁)
std::cout << "线程:更新数据后,开始耗时操作...\n";
std::this_thread::sleep_for(std::chrono::seconds(3)); // 长时间持有锁
// 其他线程需等待3秒才能获取锁,可能引发死锁
}
// 正确示例:用unique_lock提前解锁,仅在必要时持有锁
void correct_worker() {
std::unique_lock<std::mutex> lock(mtx);
shared_data++; // 操作共享资源(需持有锁)
lock.unlock(); // 提前释放锁,无需等到析构
// 耗时操作在锁外执行,不阻塞其他线程
std::cout << "线程:更新数据后释放锁,开始耗时操作...\n";
std::this_thread::sleep_for(std::chrono::seconds(3));
}
int main() {
// 测试错误场景(锁被长时间持有)
// std::thread t1(wrong_worker);
// std::thread t2(wrong_worker);
// 测试正确场景(锁提前释放)
std::thread t1(correct_worker);
std::thread t2(correct_worker);
t1.join();
t2.join();
std::cout << "最终数据:" << shared_data << std::endl;
return 0;
}
关键:unique_lock::unlock() 允许在共享资源操作完成后立即释放锁,而非等到作用域结束,大幅减少其他线程的等待时间,降低死锁概率。
3. 配合条件变量,避免等待时持有锁
死锁场景:线程在等待条件变量(如生产者-消费者中的“队列非空”)时,若持有锁不放,其他线程无法更新条件,导致永久等待(死锁)。
解决方法:condition_variable::wait() 会自动释放 unique_lock 持有的锁,并在被唤醒时重新获取,确保等待时不阻塞其他线程。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> q;
std::mutex mtx;
std::condition_variable cv;
// 消费者:等待队列非空
void consumer() {
while (true) {
std::unique_lock<std::mutex> lock(mtx); // 必须用unique_lock
// wait()会自动释放锁,并阻塞等待;被唤醒后重新获取锁
cv.wait(lock, []{ return !q.empty(); }); // 条件不满足时释放锁
// 此时已重新获取锁,安全操作共享资源
int data = q.front();
q.pop();
std::cout << "消费:" << data << "(队列大小:" << q.size() << ")\n";
if (data == -1) break; // 退出标志
}
}
// 生产者:往队列放数据
void producer() {
for (int i = 0; i < 3; ++i) {
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lock(mtx);
q.push(i);
std::cout << "生产:" << i << "(队列大小:" << q.size() << ")\n";
}
cv.notify_one(); // 通知消费者
}
// 发送退出标志
std::lock_guard<std::mutex> lock(mtx);
q.push(-1);
cv.notify_one();
}
int main() {
std::thread c(consumer);
std::thread p(producer);
c.join();
p.join();
return 0;
}
关键:unique_lock 是 condition_variable::wait() 的唯一合法参数,因为 wait() 需要在等待时释放锁、唤醒时重新获取,而 lock_guard 不支持手动释放,无法满足此需求(若用 lock_guard 会导致死锁)。
4. 移动语义转移锁所有权,避免重复加锁
死锁风险:函数间传递锁时,若通过复制而非转移,可能导致重复加锁(同一线程多次锁定同一互斥锁,导致未定义行为)。
解决方法:unique_lock 支持移动语义(std::move),可安全转移锁的所有权,避免重复锁定。
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
// 接收锁的所有权并操作
void process_data(std::unique_lock<std::mutex> lock) { // 按值接收(移动语义)
std::cout << "子函数:已获取锁,处理数据...\n";
// 锁会在函数结束时自动释放(无需手动操作)
}
// 主函数:获取锁并转移给子函数
void main_task() {
std::unique_lock<std::mutex> lock(mtx); // 获取锁
std::cout << "主函数:获取锁,准备转移...\n";
// 转移锁的所有权给子函数(避免主函数和子函数同时持有锁)
process_data(std::move(lock)); // 移动后,lock不再持有锁
// 此时lock已释放,其他线程可获取锁
std::cout << "主函数:锁已转移,继续执行...\n";
}
int main() {
std::thread t(main_task);
t.join();
return 0;
}
关键:unique_lock 不可复制,但可移动(std::move),确保锁的所有权唯一,避免同一锁被多个 unique_lock 实例持有导致的重复加锁或提前释放。
三、总结:unique_lock 避免死锁的核心用法
- 控制加锁顺序:用
std::defer_lock延迟锁定,配合std::lock()按固定顺序原子获取多把锁,避免循环等待。 - 提前释放锁:通过
unlock()手动释放,仅在操作共享资源时持有锁,减少阻塞时间。 - 配合条件变量:作为
wait()的参数,允许等待时自动释放锁,避免等待时阻塞其他线程。 - 转移锁所有权:用移动语义(
std::move)安全传递锁,避免重复加锁。
unique_lock 的灵活性使其在复杂锁场景中比 lock_guard 更能有效规避死锁,但也需注意:过度使用手动解锁可能引入错误,需平衡灵活性与安全性。
更多推荐


所有评论(0)