【问题标题】:Do I have to acquire lock before calling condition_variable.notify_one()?在调用 condition_variable.notify_one() 之前我必须获取锁吗?
【发布时间】:2013-06-10 17:10:10
【问题描述】:

我对@9​​87654323@ 的使用有点困惑。我知道在调用condition_variable.wait() 之前,我必须在mutex 上创建一个unique_lock。我找不到的是在调用notify_one()notify_all() 之前是否还应该获取唯一锁。

cppreference.com 上的示例存在冲突。例如,notify_one page 给出了这个例子:

#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()没有锁,但是后面的调用有锁。这个例子是错误的还是有一些理由?

【问题讨论】:

    标签: c++ multithreading condition-variable


    【解决方案1】:

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

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

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


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

    关键是i 会发生什么 - 这是实际指示消费者是否有“工作”要做的对象。 condition_variable 只是一种让消费者有效地等待对i 的更改的机制。

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

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

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

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

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

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

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

    如果您查看@eh9 指出的问题Sync is unreliable using std::atomic and 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-&gt;resume_mutex 时执行的。但是在第 1 步检查wait() 是否是必要的不需要同时持有该锁(更不用说连续检查和等待),这是一个要求正确使用条件变量)。我相信对该代码有问题的人 sn-p 认为由于 f-&gt;counterstd::atomic 类型,这将满足要求。但是,std::atomic 提供的原子性不会扩展到随后对f-&gt;resume.wait(lock) 的调用。在此示例中,检查f-&gt;counter(步骤#1)和调用wait()(步骤#3)之间存在竞争。

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

    【讨论】:

    • 它具有更深层次的含义:domaigne.com/blog/computing/… 值得注意的是,您提到的 pthread 问题应该通过更新版本或使用正确标志构建的版本来解决。 (启用wait morphing 优化)此链接中解释的经验法则:在超过 2 个线程的情况下,通知 WITH 锁更好,以获得更可预测的结果。
    • @Michael:据我了解,消费者最终需要致电the_condition_variable.wait(lock);。如果不需要锁来同步生产者和消费者(比如底层是一个无锁 spsc 队列),那么如果生产者不锁定它,那么这个锁就没有任何作用。我没意见。但是稀有种族没有风险吗?如果生产者没有持有锁,他不能在消费者等待之前调用 notify_one 吗?然后消费者点击等待并且不会醒来......
    • 例如在上面的代码中说,消费者在std::cout &lt;&lt; "Waiting... \n";,而生产者在cv.notify_one();,然后叫醒电话不见了......或者我在这里错过了什么?
    • @Cookie。是的,那里有一个竞争条件。见stackoverflow.com/questions/20982270/…
    • @eh9 :该死的,感谢您的评论,我刚刚找到了导致我的代码不时冻结的错误的原因。这是由于这种竞争条件的确切情况。在通知完全解决问题后解锁互斥锁...非常感谢!
    【解决方案2】:

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

    在给定的示例中,锁定是由变量i 的并发使用驱动的。因为signals 线程修改变量,它需要确保在此期间没有其他线程访问它。

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

    【讨论】:

    • 当然可以,但除此之外,它们还需要与条件变量一起使用,这样整个模式才能真正起作用。值得注意的是条件变量wait函数在调用中释放锁,并且只有在重新获得锁后才返回。在那之后,您可以安全地检查您的状况,因为您已经获得了“阅读权”,比如说。如果它仍然不是你在等待的,你回到wait。这就是模式。顺便说一句,这个例子不尊重它。
    【解决方案3】:

    情况

    使用 vc10 和 Boost 1.56,我实现了一个并发队列,非常像 this blog post 建议的那样。作者将互斥锁解锁以最小化争用,即在互斥锁解锁的情况下调用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 documentation 中的示例支持解锁互斥锁:

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

    【讨论】:

    • 您是否将此问题报告给 Boost.Thread?我在那里找不到类似的任务svn.boost.org/trac/boost/…
    • @magras 遗憾的是我没有,不知道为什么我没有考虑到这一点。不幸的是,我没有成功使用提到的队列重现此错误。
    • 我不确定我是否看到过早唤醒会导致死锁。具体来说,如果您在 push() 释放队列互斥体之后但在调用 notify_one() 之前在 pop() 中退出 cond_.wait() - Pop() 应该看到队列非空,并使用新条目而不是等待。如果你在 push() 更新队列时退出 cond_.wait(),锁应该由 push() 持有,因此 pop() 应该阻塞等待锁被释放。任何其他早期唤醒都将持有锁,阻止 push() 在 pop() 调用下一个 wait() 之前修改队列。我错过了什么?
    【解决方案4】:

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

    【讨论】:

      【解决方案5】:

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

      为了澄清,您必须在进入 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() 的时刻) 导致计数达到 -1000,因此调用 notify_one()。请注意,只有在第一行取消导致该线程失败时才会发生这种情况。

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

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

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

      出于这个原因,我们应该无法调用 notify_one()直到我们调用了 wait()。请注意,条件变量的强大之处在于它能够以原子方式解锁互斥锁,检查是否发生了对 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();
        }
      }
      

      [...相同的开始()...]

      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_*() 之前解锁互斥锁是完全可以的。

      【讨论】:

        【解决方案6】:

        正如其他人所指出的,就竞争条件和线程相关问题而言,您在调用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。线程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 当前持有的锁来进行检查.因此,我们确保cvfoo 返回后不会被线程t 访问。

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

        【讨论】:

        • 我正在寻找一个我们也需要锁定condition_variable 的示例,这是精心设计的示例和解释!
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2011-06-15
        • 1970-01-01
        • 2021-10-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-04-04
        相关资源
        最近更新 更多