在std::condition_variable - cppreference.com中给了一个多线程编程的例子。
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
void worker_thread()
{
// 等待直至 main() 发送数据
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return ready;});
// 等待后,我们占有锁。
std::cout << "Worker thread is processing data\n";
data += " after processing";
// 发送数据回 main()
processed = true;
std::cout << "Worker thread signals data processing completed\n";
// 通知前完成手动解锁,以避免等待线程才被唤醒就阻塞(细节见 notify_one )
lk.unlock();
cv.notify_one();
}
int main()
{
std::thread worker(worker_thread);
data = "Example data";
// 发送数据到 worker 线程
{
std::lock_guard<std::mutex> lk(m);
ready = true;
std::cout << "main() signals data ready for processing\n";
}
cv.notify_one();
// 等候 worker
{
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return processed;});
}
std::cout << "Back in main(), data = " << data << '\n';
worker.join();
return 0;
}
首先我们先运行一下,看一下结果:
main() signals data ready for processing
Worker thread is processing data
Worker thread signals data processing completed
Back in main(), data = Example data after processing
在main函数中,有下面这样两段花括号包含的函数调用。
在这里,我们暂时称它们为作用域func1()、作用域func2()。
// 发送数据到 worker 线程
// 称之:作用域func1()
{
std::lock_guard<std::mutex> lk(m);
ready = true;
std::cout << "main() signals data ready for processing\n";
}
// 等候 worker
// 称之:作用域func2()
{
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [] {return processed; });
}
当把作用域func1()这一对花括号去掉以后,程序发生了死锁。
程序打印了“main() signals data ready for processing”以后就卡住了。
在分析死锁前,我们需要知道std::lock_guard<std::mutex> lk(m);
类
lock_guard
是互斥体包装器,为在作用域块期间占有互斥提供便利 RAII 风格机制。创建
lock_guard
对象时,它试图接收给定互斥的所有权。控制离开创建lock_guard
对象的作用域时,销毁lock_guard
并释放互斥。
{}内的函数调用都在一个作用域内,当里面这个{},互斥锁被释放掉。
再看condition_variable的描述:
condition_variable
类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知condition_variable
。有意修改变量的线程必须
- 获得
std::mutex
(常通过 std::lock_guard )- 在保有锁时进行修改
- 在
std::condition_variable
上执行 notify_one 或 notify_all (不需要为通知保有锁)即使共享变量是原子的,也必须在互斥下修改它,以正确地发布修改到等待的线程。
wait函数的描述:
wait
导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,可选地循环直至满足某谓词。1) 原子地解锁
lock
,阻塞当前执行线程,并将它添加到于 *this 上等待的线程列表。线程将在执行 notify_all() 或 notify_one() 时被解除阻塞。解阻塞时,无关乎原因,lock
再次锁定且wait
退出。2) 等价于
while (!pred()) { wait(lock); }此重载可用于在等待特定条件成为
true
时忽略虚假唤醒。注意进入此方法前,必须得到lock
,wait(lock)
退出后也会重获得它,即能以lock
为对pred()
访问的保障。
了解上面以后,可以发现,在原有的逻辑中,work_thread中持有锁,然后到cv.wait(lk, [] {return ready; });处释放锁。然后走到作用域func1()中,func1()持锁,修改ready值为true,离开作用域后,func1()自动地就释放掉互斥锁了。然后通过cv.notify_one();通知一个等待的线程,这时候cv.wait被唤醒并判断内部lambda表达式返回值,发现是true,则重新持锁,完成后续的操作。(注:由于CPU任务的调度,主线程和work_thread线程中函数执行顺序可能稍有不同。)
如果我们将作用域func1()的花括号去掉后,那么调用完之前func1()所有的函数调用后,互斥锁还一直被main函数所在的主线程持有,执行完cv.notify_one()后,cv.wait(lk, [] {return ready; })无法重新持锁,也就无法继续执行work_thread后面的打印。
那函数调用在哪里等待了呢?
停在了作用域func2()中互斥锁申请的地方,debug的时候我们可以看到:
现在再来看demo中work_thread中的这几句,先提前释放锁,防止主线程作用域func2()在notify_one后,wait函数拿不到锁。
// 通知前完成手动解锁,以避免等待线程才被唤醒就阻塞(细节见 notify_one )
lk.unlock();
cv.notify_one();
现在再来看产生死锁的四个条件(引用自死锁产生的原因及四个必要条件 - 知乎):
(1) 互斥条件:一个资源每次只能被一个进程使用。
(2) 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3) 不可剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
(4) 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
可以发现先前分析的例子,完全满足这4个条件。
怎么解决?破坏掉任意一个条件即可。
上述例子中,产生死锁的原因是互斥锁所持有的作用域太大,导致其他函数调用想持锁时拿不到。
互斥锁的持有范围内,应尽量地只包含到并发编程中的条件变量或特定条件等。(即改为原来的demo)
还有一种解决方案:从第二个条件“请求与保持条件”入手,取消保持这个操作我们将作用域func2()的代码改成如下:
// 等候 worker
{
// 构造函数,第二个参数传入std::try_to_lock,尝试获得互斥的所有权而不阻塞
std::unique_lock<std::mutex> lk(m, std::try_to_lock);
cv.wait(lk, [] {return processed; });
}
为了验证该问题,我们在work_thread释放锁之前加一个5秒的sleep。
void worker_thread()
{
// 等待直至 main() 发送数据
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, [] {return ready; });
// 等待后,我们占有锁。
std::cout << getCurUtcTime() << "Worker thread is processing data\n";
data += " after processing";
// 发送数据回 main()
processed = true;
std::cout << getCurUtcTime() << "Worker thread signals data processing completed\n";
// sleep 5 seconds
std::chrono::milliseconds time(5000);
std::this_thread::sleep_for(time);
// 通知前完成手动解锁,以避免等待线程才被唤醒就阻塞(细节见 notify_one )
lk.unlock();
cv.notify_one();
}
另外添加一个打印时间的函数:
char* getCurUtcTime() {
time_t now = time(0);
return asctime(gmtime(&now));
}
运行一下,死锁解决。
Fri Apr 28 03:36:13 2023
main() signals data ready for processing
Fri Apr 28 03:36:13 2023
Worker thread is processing data
Fri Apr 28 03:36:13 2023
Worker thread signals data processing completed
Fri Apr 28 03:36:18 2023
Back in main(), data = Example data after processing
还有什么方法呢?
unique_lock有一个移动构造函数unique_lock(unique_lock&& other) noexcept;,other是要移动的unique_lock对象。移动构造函数将other的所有权转移到新创建的unique_lock对象中,并将other置为无效状态。
因此可以将作用域func2()改成如下,也解决了问题。
{
std::unique_lock<std::mutex> lk2(std::move(lk1));
cv.wait(lk2, [] {return processed; });
}