繁体   English   中英

在调用 condition_variable.notify_one() 之前我必须获取锁吗?

[英]Do I have to acquire lock before calling condition_variable.notify_one()?

我对std::condition_variable的使用有点困惑。 我知道在调用condition_variable.wait()之前,我必须在mutex锁上创建一个unique_lock 我找不到的是在调用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()没有锁,但后续调用有。 这个例子是错误的还是有一些理由?

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

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

请记住, while循环中的lock()调用在某些时候是必要的,因为在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 i调用condition_variable::wait()之间进行更改。 这是正确使用条件变量的关键。

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

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

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

  • 如果i为 0 则消费者调用cv.wait() ,那么当调用实现的wait(lock)部分时i仍将为 0 - 正确使用锁可确保这一点。 在这种情况下,生产者没有机会在其while循环中调用condition_variable::notify_one()直到消费者调用cv.wait(lk, []{return i == 1;}) (和wait() call 已经完成了正确“捕获”通知所需的一切 - wait()在完成之前不会释放锁)。 所以在这种情况下,消费者不能错过通知。

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

这里的示例确实具有额外的复杂性,即使用done变量向生产者线程发信号通知消费者已经认识到i == 1 ,但我认为这根本不会改变分析,因为所有对done的访问(用于阅读和修改)在涉及icondition_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)
}

您会注意到 #3 处的wait()是在按住f->resume_mutex时执行的。 但是在第 1 步检查是否需要wait()没有在完​​全持有该锁的情况下完成(对于检查和等待来说更不连续),这是正确使用条件变量的要求) . 我相信对该代码片段有问题的人认为,由于f->counterstd::atomic类型,因此可以满足要求。 但是, std::atomic提供的原子性不会扩展到随后对f->resume.wait(lock)的调用。 在此示例中,检查f->counter (步骤 #1)和调用wait() (步骤 #3)之间存在竞争。

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

正如其他人指出的那样,就竞争条件和线程相关问题而言,您在调用notify_one()时不需要持有锁。 但是,在某些情况下,可能需要持有锁以防止condition_variablenotify_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。 线程t获取锁 (1),设置谓词变量 (2),然后释放锁 (3)。 假设在执行notify_one() (4) 之前此时有另一个上下文切换。 主线程获取锁(6)并执行第(7)行,此时谓词返回true并且没有理由等待,因此它释放锁并继续。 foo返回 (8) 并且其范围内的变量(包括cv )被销毁。 在线程t可以加入主线程 (9) 之前,它必须完成它的执行,所以它从停止的地方继续执行cv.notify_one() (4),此时cv已经被销毁!

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

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

情况

使用 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()之前不能解锁互斥锁,尽管这两种方式似乎都是正确的。

只是添加这个答案,因为我认为接受的答案可能会产生误导。 在所有情况下,您都需要在调用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_*() 之前解锁互斥锁是完全可以的。

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

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

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

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

据我了解,notify_one 调用 pthread_cond_signal。 如果是这样,那么对此有何看法?

为了可预测的调度行为和防止丢失唤醒,互斥体应在发出条件变量信号时保持。

https://www.unix.com/man-page/hpux/3T/pthread_cond_signal/

所有等待条件变量的线程都被挂起,直到另一个线程使用信号函数:

pthread_cond_signal(&myConVar);

在这种情况下,必须在调用函数之前锁定互斥锁,然后再解锁。

https://www.i-programmer.info/programming/cc/12288-fundamental-c-condition-variables.html

我个人遇到过通知丢失的情况,因为在没有锁定互斥锁的情况下调用了 notify_one。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM