this page中,编写了此示例代码以说明如何使用notify_one:

#include <iostream>
#include <condition_variable>
#include <thread>
#include <chrono>

std::condition_variable cv;
std::mutex cv_m;
int i = 0;
bool done = false;

void waits()
{
    std::unique_lock<std::mutex> lk(cv_m);
    std::cout << "Waiting... \n";
    cv.wait(lk, []{return i == 1;});
    std::cout << "...finished waiting. i == 1\n";
    done = true;
}

void signals()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "Notifying...\n";
    cv.notify_one();

    std::unique_lock<std::mutex> lk(cv_m);
    i = 1;
    while (!done) {
        lk.unlock();
        std::this_thread::sleep_for(std::chrono::seconds(1));
        lk.lock();
        std::cerr << "Notifying again...\n";
        cv.notify_one();
    }
}

int main()
{
    std::thread t1(waits), t2(signals);
    t1.join(); t2.join();
}

但是,valgrind(实际上是hegrind)提示:



如果第二个线程在第一个线程之前运行,并且在其他任何线程之前到达cv.notify_one();,它将在不持有任何锁的情况下向其他线程发出信号。

我实际上正在学习如何使用这些condition variables,并试图了解谁应该锁定/解锁与它们关联的互斥锁。所以我的问题是:这段代码执行正确吗?还是Helgrind错误?

最佳答案

[广告的真相:直到最近,我还是与Helgrind/Valgrind竞争的商业数据竞赛和内存错误检测器的设计师。]

您的代码中没有数据争用。 Helgrind发出此警告是因为对条件变量的工作方式有些微妙。 the "hints" section of the Helgrind manual中对此进行了一些讨论。简短地说:Helgrind正在做-在检测数据竞争之前发生的事情。它通过观察代码调用pthread_mutex_lock/unlock和pthread_cond_wait/signal的顺序(这些是实现C++ 11原语的C原语)来推导“先发生”关系。

如果遵循遵循以下原则的原则,即cv.notify_one()调用始终受围绕相应cv.wait()调用的同一个互斥锁保护,则Helgrind知道互斥体将强制执行正确的事前发生关系,因此一切都会好起来的。

在您的情况下,Helgrind提示在获得cv.notify_one()的锁定之前,在signals()的顶部进行了最初的(免费的)cv_m调用。它知道这是一种可能使它感到困惑的情况(尽管真正的困惑是它以后可能会报告误报,因此此处的警告消息有点误导人。)

请注意,这是Helgrind手册提示部分中“使用信号量而不是condition_variables”的建议。与工具和人类的条件变量相比,信号量的正确性要难得多。信号量是“太笼统的”,因为您不能依赖各种变量。使用信号量“锁定”的同一线程不必是“解锁”的线程。在非二进制信号量上“等待”的两个线程可能具有或可能没有事前发生的关系。因此,如果您试图推理(或自动检测)死锁或数据争用情况,则信号量几乎没有用。

更好的建议是使用条件变量来发出信号/等待,但要确保遵循一定的规则,即对特定条件变量的所有调用都发生在受同一互斥锁保护的关键部分内。

09-27 11:27