Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Mq-b opened this issue May 30, 2024 · 1 comment
Labels
bug Something isn't working enhancement 内容改进与增强 issue-resolved 问题已解决

Comments

@Mq-b
Copy link
Owner

Mq-b commented May 30, 2024

说明问题

原文:

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

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;
}

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

运行测试。

@Mq-b Mq-b added bug Something isn't working enhancement 内容改进与增强 labels May 30, 2024
Mq-b added a commit that referenced this issue May 30, 2024
2. 修改展示条件变量超时功能的示例 #15
3. C++20 信号量(完成本节部分内容)#12
@Mq-b Mq-b added resolved 此问题已解决 issue-resolved 问题已解决 and removed resolved 此问题已解决 labels May 30, 2024
@rsp4jack
Copy link

rsp4jack commented Jun 1, 2024

既然这个 issue 已经被 resolved 了,为何不 close 它?

@Mq-b Mq-b closed this as completed Jun 2, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working enhancement 内容改进与增强 issue-resolved 问题已解决
Projects
None yet
Development

No branches or pull requests

2 participants