Closed
Description
- 位置。
说明问题
原文:
等待条件变量满足条件——带超时功能
using namespace std::chrono_literals; std::condition_variable cv; bool done{}; std::mutex m; bool wait_loop(){ const auto timeout = std::chrono::steady_clock::now() + 500ms; std::unique_lock<std::mutex> lk{ m }; while(!done){ if(cv.wait_until(lk,timeout) == std::cv_status::timeout){ std::cout << "超时 500ms\n"; break; } } return done; }运行测试。
这段代码意图表达使用条件变量的超时功能,即使用 wait_until
函数,并提供了 godbolt 的完整运行示例。此时看不出什么问题,完整代码如下所示:
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <chrono>
#include <thread>
#include <future>
using namespace std::chrono_literals;
std::condition_variable cv;
bool done{};
std::mutex m;
bool wait_loop(){
const auto timeout = std::chrono::steady_clock::now() + 500ms;
std::unique_lock<std::mutex> lk{ m };
while(!done){
if(cv.wait_until(lk,timeout) == std::cv_status::timeout){
std::cout << "超时 500ms\n";
break;
}
}
return done;
}
void trigger(){
{
std::lock_guard<std::mutex> lk{ m };
done = true;
}
cv.notify_one();
}
int main() {
auto future = std::async(std::launch::async, wait_loop);
std::thread t{ trigger };
t.join();
if(future.get()){
std::cout << "没有超时\n";
}else{
std::cout << "超时\n";
}
}
运行结果:
没有超时
然而大多数读者会尝试修改此代码,希望直接在 trigger
函数添加 sleep 延时,超过 500ms 就会打印输出:
超时 500ms
超时
想法很美好,如果延时是添加在 lock_guard 之前,即互斥量上锁(lock)之前,那么没有问题,可以得到我们预期的结果
void trigger() {
{
std::this_thread::sleep_for(1s);
std::lock_guard<std::mutex> lk{ m };
done = true;
}
cv.notify_one();
}
运行测试。
但是,如果这个延时,是在 lock_guard 之后,即上锁(lock)之后,那么就会出现问题。
void trigger() {
{
std::lock_guard<std::mutex> lk{ m };
std::this_thread::sleep_for(1s);
done = true;
}
cv.notify_one();
}
运行结果:
超时 500ms
没有超时
是不是感到无法理解?前后矛盾?那么产生这个问题的原因是什么呢?
解释原因
我们为代码添加一些打印日志,完整如下:
#include <iostream>
#include <condition_variable>
#include <mutex>
#include <chrono>
#include <thread>
#include <future>
using namespace std::chrono_literals;
std::condition_variable cv;
bool done{};
std::mutex m;
bool wait_loop() {
const auto timeout = std::chrono::steady_clock::now() + 500ms;
std::unique_lock<std::mutex> lk{ m };
std::cout << "wait_loop\n";
while (!done) {
if (cv.wait_until(lk, timeout) == std::cv_status::timeout) {
std::cout << "超时 500ms" << '\n';
break;
}
}
return done;
}
void trigger() {
{
std::lock_guard<std::mutex> lk{ m };
std::this_thread::sleep_for(std::chrono::seconds(2));
done = true;
std::cout << "trigger 延时结束" << '\n';
}
cv.notify_one();
}
int main() {
auto future = std::async(std::launch::async, wait_loop);
std::thread t{ trigger };
t.join();
if (future.get()) {
std::cout << "没有超时\n";
}
else {
std::cout << "超时\n";
}
}
运行结果:
wait_loop
trigger 延时结束
超时 500ms
没有超时
我们来研究一下
- wait_loop 线程拿到锁,开始执行,打印“wait_loop”,不满足条件,进入循环,调用
cv.wait_until(lk, timeout)
;也就是解锁互斥量,直到“虚假唤醒”、其它线程调用唤醒函数、超时,才会唤醒。 - 因为 wait_loop 线程解锁互斥量,trigger 线程得以拿到锁,开始执行,直接延时 2s。(注意,重点来了)在这两秒中,wait_loop 线程中阻塞的
wait_until
调用超时,解除阻塞,但是需要:在解除阻塞时调用lock.lock()
,然而根本无法成功调用,会一直阻塞,因为目前锁被trigger
线程持有,还未释放(unlock)。 - trigger 线程延时两秒结束,给 done 赋为 true,打印:“trigger 延时结束”。离开这个作用域,调用 lock_guard 的析构函数,解锁互斥量(unlock)。
- 因为 trigger 终于解锁(unlock),wait_loop 线程得以成功上锁(lock),往下执行。打印“超时 500ms”,然后
break
。return done
。然而此时done
已经被设置为true
。 - 所以
future.get()
返回true
,打印:“没有超时”。
我没有提 cv.notify_one();
,因为不重要,它会因为超时而解除阻塞,而不是等到最后调用函数唤醒。
我们解释了这个问题,修改方式非常简单,别返回 done,直接 return true
、return false
。
bool wait_loop() {
const auto timeout = std::chrono::steady_clock::now() + 500ms;
std::unique_lock<std::mutex> lk{ m };
while (!done) {
if (cv.wait_until(lk, timeout) == std::cv_status::timeout) {
std::cout << "超时 500ms\n";
return false;
}
}
return true;
}
也就不存在上面的问题了。
运行测试。