97

我对std::condition_variable. 我知道我必须在调用之前创建unique_lock一个。我找不到的是在调用or之前是否还应该获取唯一锁。mutexcondition_variable.wait()notify_one()notify_all()

cppreference.com上的示例相互矛盾。例如,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();
}

这里的锁不是为第一个获得的notify_one(),而是为第二个获得的notify_one()。通过示例查看其他页面,我看到了不同的东西,主要是没有获得锁。

  • 我可以选择自己在调用之前锁定互斥锁notify_one(),为什么我会选择锁定它?
  • 在给出的示例中,为什么第一个没有锁notify_one(),但后续调用有。这个例子是错误的还是有一些理由?
4

6 回答 6

86

调用时您不需要持有锁condition_variable::notify_one(),但从某种意义上说,它仍然是明确定义的行为而不是错误,这并没有错。

但是,这可能是一种“悲观化”,因为任何等待线程变为可运行(如果有)都会立即尝试获取通知线程持有的锁。notify_one()我认为在调用or时避免持有与条件变量关联的锁是一个很好的经验法则notify_all()。请参阅Pthread Mutex:pthread_mutex_unlock() 消耗大量时间的示例,其中在调用 pthread 之前释放锁相当于显着notify_one()提高了性能。

请记住,循环中的lock()调用在某些时候是必要的,因为在循环条件检查while期间需要保持锁定。while (!done)但不需要为调用notify_one().


2016-02-27:大型更新解决了评论中关于是否存在竞争条件的一些问题,即锁定对notify_one()调用没有帮助。我知道这个更新迟了,因为这个问题是在大约两年前被问到的,但我想解决@Cookie 的问题,即如果生产者(在本例中)在消费者(在本例中)之前signals()调用时可能出现的竞争条件是能打电话。notify_one()waits()wait()

关键是发生了什么i——这个对象实际上表明了消费者是否有“工作”要做。这condition_variable只是一种让消费者有效地等待更改的机制i

生产者在更新时需要持有锁i,消费者在检查i和调用时必须持有锁condition_variable::wait()(如果它需要等待的话)。在这种情况下,关键是当消费者进行检查和等待时,它必须是持有锁的同一个实例(通常称为临界区)。由于临界区在生产者更新i和消费者检查并等待时保持i,因此没有机会在消费者检查和调用时i之间进行更改。这是正确使用条件变量的关键。icondition_variable::wait()

C++ 标准规定 condition_variable::wait() 在使用谓词调用时的行为如下所示(如本例所示):

while (!pred())
    wait(lock);

消费者检查时可能会出现两种情况i

  • 如果i为 0 则消费者调用cv.wait(),则在调用部分实现i时仍将为 0 wait(lock)- 正确使用锁可确保这一点。在这种情况下,生产者在消费者调用之前没有机会condition_variable::notify_one()在其循环中调用)。所以在这种情况下,消费者不能错过通知。whilecv.wait(lk, []{return i == 1;})wait()wait()

  • 如果i消费者调用时 if 已经为 1 ,则永远不会调用cv.wait()该部分的实现,因为测试将导致内部循环终止。在这种情况下,调用 notify_one() 的时间并不重要——消费者不会阻塞。wait(lock)while (!pred())

这里的示例确实具有额外的复杂性,即使用done变量向生产者线程发信号,消费者已经认识到这一点i == 1,但我认为这根本不会改变分析,因为所有访问done(用于读取和修改) ) 在涉及i和 的相同关键部分中完成condition_variable

如果您查看@eh9 指出的问题,Sync is unreliable using std::atomic 和 std::condition_variable,您看到竞争条件。但是,该问题中发布的代码违反了使用条件变量的基本规则之一:在执行检查和等待时,它不包含单个关键部分。

在该示例中,代码如下所示:

if (--f->counter == 0)      // (1)
    // we have zeroed this fence's counter, wake up everyone that waits
    f->resume.notify_all(); // (2)
else
{
    unique_lock<mutex> lock(f->resume_mutex);
    f->resume.wait(lock);   // (3)
}

您会注意到wait()在 #3 的位置是在按住 时执行的f->resume_mutexwait()但是在第 1 步检查是否有必要在完全保持该锁的同时(更不用说检查和等待的连续性)进行检查,这是正确使用条件变量的要求)。我相信对该代码片段有疑问的人认为,因为f->counter它是一种std::atomic类型,所以可以满足要求。但是,由 提供的原子性std::atomic不会扩展到后续调用f->resume.wait(lock). f->counter在此示例中,检查时间(步骤#1)和wait()调用时间(步骤#3)之间存在竞争。

此问题的示例中不存在该种族。

于 2013-06-14T06:06:18.263 回答
11

notify_one()正如其他人指出的那样,就竞争条件和线程相关问题而言,您不需要在调用时持有锁。但是,在某些情况下,可能需要持有锁以防止在调用condition_variable之前notify_one()被破坏。考虑以下示例:

thread t;

void foo() {
    std::mutex m;
    std::condition_variable cv;
    bool done = false;

    t = std::thread([&]() {
        {
            std::lock_guard<std::mutex> l(m);  // (1)
            done = true;  // (2)
        }  // (3)
        cv.notify_one();  // (4)
    });  // (5)

    std::unique_lock<std::mutex> lock(m);  // (6)
    cv.wait(lock, [&done]() { return done; });  // (7)
}

void main() {
    foo();  // (8)
    t.join();  // (9)
}

t假设在我们创建新线程之后但在我们开始等待条件变量之前(介于 (5) 和 (6) 之间),有一个上下文切换到新创建的线程。线程t获取锁 (1),设置谓词变量 (2),然后释放锁 (3)。notify_one()假设在执行 (4)之前此时有另一个上下文切换。主线程获得锁(6)并执行第(7)行,此时谓词返回true并且没有理由等待,因此它释放锁并继续。foo返回 (8) 并且其范围内的变量(包括cv)被销毁。在线程t可以加入主线程(9)之前,它必须完成它的执行,所以它从它离开的地方继续执行cv.notify_one()(4)、此时cv已经被破坏了!

在这种情况下,可能的解决方法是在调用时保持锁定notify_one(即删除以第 (3) 行结尾的范围)。通过这样做,我们确保之前的线程t调用可以检查新设置的谓词变量并继续,因为它需要获取 当前持有的锁来进行检查。因此,我们确保返回后不被线程访问。notify_onecv.waittcvtfoo

总而言之,这种特定情况下的问题实际上与线程无关,而是与通过引用捕获的变量的生命周期有关。cv通过 thread 引用捕获t,因此您必须确保cv在线程执行期间保持活动状态。此处介绍的其他示例不会遇到此问题,因为condition_variablemutex对象是在全局范围内定义的,因此可以保证它们在程序退出之前一直保持活动状态。

于 2018-10-23T13:35:39.423 回答
10

情况

使用 vc10 和 Boost 1.56,我实现了一个并发队列,就像这篇博文所建议的那样。作者将互斥锁解锁以最小化争用,即notify_one()在互斥锁解锁的情况下调用:

void push(const T& item)
{
  std::unique_lock<std::mutex> mlock(mutex_);
  queue_.push(item);
  mlock.unlock();     // unlock before notificiation to minimize mutex contention
  cond_.notify_one(); // notify one waiting thread
}

Boost 文档中的示例支持解锁互斥锁:

void prepare_data_for_processing()
{
    retrieve_data();
    prepare_data();
    {
        boost::lock_guard<boost::mutex> lock(mut);
        data_ready=true;
    }
    cond.notify_one();
}

问题

这仍然导致以下不稳定的行为:

  • 虽然notify_one()没有被调用,但cond_.wait()仍然可以通过boost::thread::interrupt()
  • 一次notify_one()被称为第一次cond_.wait()死锁;等待不能由boost::thread::interrupt()boost::condition_variable::notify_*()不再结束。

解决方案

删除该行使mlock.unlock()代码按预期工作(通知和中断结束等待)。请注意,notify_one()在互斥锁仍然锁定的情况下调用它,然后在离开作用域时立即解锁:

void push(const T& item)
{
  std::lock_guard<std::mutex> mlock(mutex_);
  queue_.push(item);
  cond_.notify_one(); // notify one waiting thread
}

这意味着至少在我的特定线程实现中,互斥锁在调用之前不能被解锁boost::condition_variable::notify_one(),尽管这两种方式似乎都是正确的。

于 2015-06-18T13:58:05.100 回答
2

只是添加这个答案,因为我认为接受的答案可能会产生误导。在所有情况下,您都需要在调用 notify_one() 之前锁定互斥锁,以使您的代码成为线程安全的,尽管您可能会在实际调用 notify_*() 之前再次解锁它。

澄清一下,您必须在进入 wait(lk) 之前获取锁,因为 wait() 会解锁 lk,如果锁没有被锁定,这将是未定义的行为。notify_one() 不是这种情况,但您需要确保在进入 wait()让该调用解锁互斥锁之前不会调用 notify_*();这显然只能通过在调用 notify_*() 之前锁定同一个互斥锁来完成。

例如,考虑以下情况:

std::atomic_int count;
std::mutex cancel_mutex;
std::condition_variable cancel_cv;

void stop()
{
  if (count.fetch_sub(1) == -999) // Reached -1000 ?
    cv.notify_one();
}

bool start()
{
  if (count.fetch_add(1) >= 0)
    return true;
  // Failure.
  stop();
  return false;
}

void cancel()
{
  if (count.fetch_sub(1000) == 0)  // Reached -1000?
    return;
  // Wait till count reached -1000.
  std::unique_lock<std::mutex> lk(cancel_mutex);
  cancel_cv.wait(lk);
}

警告:此代码包含错误。

想法如下:线程成对调用 start() 和 stop(),但只要 start() 返回 true。例如:

if (start())
{
  // Do stuff
  stop();
}

一个(另一个)线程在某个时候会调用 cancel(),并且在从 cancel() 返回后会销毁“Do stuff”所需的对象。但是,当 start() 和 stop() 之间有线程时,cancel() 应该不会返回,并且一旦 cancel() 执行了第一行,start() 将始终返回 false,因此不会有新线程进入 'Do东西的区域。

工作正常吗?

推理如下:

1) 如果任何线程成功执行 start() 的第一行(因此将返回 true),那么还没有线程执行 cancel() 的第一行(我们假设线程总数远小于 1000方法)。

2)另外,当一个线程成功执行了 start() 的第一行,但还没有执行 stop() 的第一行,那么任何线程都不可能成功执行 cancel() 的第一行(注意只有一个线程曾经调用 cancel()):fetch_sub(1000) 返回的值将大于 0。

3) 一旦线程执行了cancel() 的第一行,start() 的第一行将始终返回false,并且调用start() 的线程将不再进入'Do stuff' 区域。

4) start() 和 stop() 的调用次数总是平衡的,所以在第一行 cancel() 执行失败后,总会有一个时刻(最后一次)调用 stop() 导致 count达到 -1000 并因此调用 notify_one()。请注意,只有在第一行取消导致该线程失败时才会发生这种情况。

除了这么多线程正在调用 start()/stop() 计数永远不会达到 -1000 并且 cancel() 永远不会返回的饥饿问题(人们可能会接受它为“不太可能且永远不会持续很长时间”)之外,还有另一个错误:

'Do stuff' 区域内可能有一个线程,可以说它只是调用 stop(); 在那一刻,一个线程执行 cancel() 的第一行,使用 fetch_sub(1000) 读取值 1 并失败。但在它使用互斥锁和/或调用wait(lk)之前,第一个线程执行stop()的第一行,读取-999并调用cv.notify_one()!

然后在我们等待条件变量之前完成对 notify_one() 的调用!并且程序将无限期地死锁。

由于这个原因,在调用 wait()之前,我们不应该调用 notify_one( )。请注意,条件变量的强大之处在于它能够以原子方式解锁互斥锁,检查是否发生了对 notify_one() 的调用并进入睡眠状态。您无法欺骗它,但是您确实需要在对可能将条件从 false 更改为 true 的变量进行更改时保持互斥锁锁定,并在调用 notify_one() 时保持锁定,因为这里描述的竞争条件

然而,在这个例子中没有条件。为什么我不使用条件'count == -1000'?因为这在这里一点也不有趣:只要达到 -1000,我们就确定没有新线程将进入“做事”区域。此外,线程仍然可以调用 start() 并且会增加计数(到 -999 和 -998 等),但我们并不关心这一点。唯一重要的是达到了 -1000 - 这样我们就可以肯定地知道“做事”区域中不再有线程了。我们确信在调用 notify_one() 时就是这种情况,但是如何确保在 cancel() 锁定其互斥体之前不调用 notify_one() 呢?只是在 notify_one() 之前不久锁定 cancel_mutex 当然不会有帮助。

问题是,尽管我们没有等待条件,但仍然存在条件,我们需要锁定互斥锁

1) 在达到该条件之前 2) 在我们调用 notify_one 之前。

因此正确的代码变为:

void stop()
{
  if (count.fetch_sub(1) == -999) // Reached -1000 ?
  {
    cancel_mutex.lock();
    cancel_mutex.unlock();
    cv.notify_one();
  }
}

[...相同的 start()...]

void cancel()
{
  std::unique_lock<std::mutex> lk(cancel_mutex);
  if (count.fetch_sub(1000) == 0)
    return;
  cancel_cv.wait(lk);
}

当然这只是一个例子,但其他情况非常相似;在几乎所有使用条件变量的情况下,您都需要在调用 notify_one() 之前(不久)锁定该互斥体,否则您可以在调用 wait() 之前调用它。

请注意,在这种情况下,我在调用 notify_one() 之前解锁了互斥锁,因为否则调用 notify_one() 有可能唤醒等待条件变量的线程,然后该线程将尝试获取互斥锁和块,在我们再次释放互斥锁之前。这只是比需要的慢一点。

这个例子有点特别,因为改变条件的那一行是由调用 wait() 的同一个线程执行的。

更常见的情况是一个线程简单地等待一个条件变为真,而另一个线程在更改该条件中涉及的变量之前获取锁(导致它可能变为真)。在这种情况下,互斥锁在条件变为真之前(和之后)立即锁定 - 因此在这种情况下,在调用 notify_*() 之前解锁互斥锁是完全可以的。

于 2018-10-05T18:16:42.233 回答
1

@Michael Burr 是正确的。condition_variable::notify_one不需要锁定变量。但是,没有什么可以阻止您在这种情况下使用锁,如示例所示。

在给定的示例中,锁的动机是同时使用变量i。因为signals线程修改了变量,所以需要确保在那段时间没有其他线程访问它。

锁用于任何需要同步的情况,我认为我们不能用更一般的方式来说明它。

于 2013-06-14T06:20:27.413 回答
0

在某些情况下,当 cv 可能被其他线程占用(锁定)时。您需要在 notify_*() 之前获得锁定并释放它。
如果不是,则 notify_*() 可能根本不执行。

于 2018-03-05T12:34:58.570 回答