Skip to content

第四章同步操作中的限时等待一节的最后一个代码示例 wait_loop 函数可能存在误导与错误 #15

Closed
@Mq-b

Description

@Mq-b

说明问题

原文:

等待条件变量满足条件——带超时功能

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
没有超时

我们来研究一下

  1. wait_loop 线程拿到锁,开始执行,打印“wait_loop”,不满足条件,进入循环,调用 cv.wait_until(lk, timeout);也就是解锁互斥量,直到“虚假唤醒”、其它线程调用唤醒函数、超时,才会唤醒。
  2. 因为 wait_loop 线程解锁互斥量,trigger 线程得以拿到锁,开始执行,直接延时 2s。(注意,重点来了)在这两秒中,wait_loop 线程中阻塞的 wait_until 调用超时,解除阻塞,但是需要:在解除阻塞时调用 lock.lock(),然而根本无法成功调用,会一直阻塞,因为目前锁被 trigger 线程持有,还未释放(unlock)。
  3. trigger 线程延时两秒结束,给 done 赋为 true,打印:“trigger 延时结束”。离开这个作用域,调用 lock_guard 的析构函数,解锁互斥量(unlock)。
  4. 因为 trigger 终于解锁(unlock),wait_loop 线程得以成功上锁(lock),往下执行。打印“超时 500ms”,然后 breakreturn done。然而此时 done 已经被设置为 true
  5. 所以 future.get() 返回 true,打印:“没有超时”。

我没有提 cv.notify_one();,因为不重要,它会因为超时而解除阻塞,而不是等到最后调用函数唤醒。


我们解释了这个问题,修改方式非常简单,别返回 done,直接 return truereturn 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;
}

也就不存在上面的问题了。

运行测试。

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancement内容改进与增强issue-resolved问题已解决

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions