C++11 多线程编程之条件变量std:: condition_variable、wait()、notify_one()、notify_all()

1、为何引入条件变量 ?

  • 解决while不断循环收发消息,让它只有消息到来时才进行处理。大大减少CPU的使用率和提高程序效率;
  • 在多线程编程中,当多个线程之间需要进行某些同步机制时,如某个线程的执行需要另一个线程完成后才能进行,可以使用条件变量;
  • c++11提供的 condition_variable 类是一个同步原语,它能够阻塞一个或者多个线程,直到另一线程修改共享变量并通知 condition_variable。也可以把它理解为信号通知机制,一个线程负责发送信号,其他线程等待该信号的触发。

2、条件变量 std:: condition_variable

std:: condition_variable是条件变量,实际上是个类,是一个与条件相关的类,说白了就是等待一个条件的达成。这个类是需要和互斥量来配合工作的,使用时定义一个该类对象即可。

实例代码:
线程A:循环等待一个条件满足,但若条件不满足会休眠在条件变量,并不会占用CPU。
线程B:专门往消息队列扔消息(数据),然后通知其它线程。

  •  当 std::condition_variable 对象的某个 wait 函数被调用的时候,它使用 std::unique_lock(通过 std::mutex) 锁住当前线程。当前线程会一直被阻塞,直到另外一个线程相同的 std::condition_variable 对象上调用了 notification 函数来唤醒当前线程;
  • std::condition_variable 对象通常使用 std::unique_lock 来等待,如果需要使用另外的 lockable 类型,可以使用 std::condition_variable_any 类,本文后面会讲到 std::condition_variable_any 的用法;

注意:

  • std :: condition_variable 仅适用于 std::unique_lock, 此限制允许在某些平台上获得最大效率。 std :: condition_variable_any 提供可与任何BasicLockable对象一起使用的条件变量,例如std :: shared_lock。
  • 修改共享变量 ready/processed 时需要锁,共享变量用于避免虚假唤醒
  •   cv.wait 第一个参数必须是 unique_lock,因为它内部会执行 unlock和lock,如果需要设置超时,使用 wait_for/wait_until

特别注意,如果不使用共享变量,当通知线程在接收线程准备接收之前发送通知,接收线程将要永远阻塞了。这里,共享变量已经置位,所以它也能避免丢失唤醒。

3、notify_one()与notify_all()

notify_one()notify_all() 常用来唤醒阻塞的线程;

  • notify_one():因为只唤醒等待队列中的第一个线程;不存在锁争用,所以能够立即获得锁;其余的线程不会被唤醒,需要等待再次调用notify_one()或者notify_all();
  • notify_all():会唤醒所有等待队列中阻塞的线程,存在锁争用,只有一个线程能够获得锁。那其余未获取锁的线程接着会怎么样?会阻塞?还是继续尝试获得锁?答案是会继续尝试获得锁(类似于轮询),而不会再次阻塞。当持有锁的线程释放锁时,这些线程中的一个会获得锁。而其余的会接着尝试获得锁。

看下面的例子:

#include                 // std::cout
#include                   // std::thread
#include                    // std::mutex, std::unique_lock
#include       // std::condition_variable
 
std::mutex mtx; // 全局互斥锁.
std::condition_variable cv; // 全局条件变量.
bool ready = false; // 全局标志位.
 
void do_print_id(int id)
{
    std::unique_lock  lck(mtx);

    while (!ready) // 如果标志位ready=false,!ready = true时, 则执行cv.wait(lck),当前线程被阻塞;
        cv.wait(lck); /*当全局标志位ready=true 之后,不执行 cv.wait(lck),
                      且另一个线程有 cv.notify_all()时,线程被唤醒, 继续往下执行打印线程编号id.*/

    std::cout << "thread " << id << '\n';
}
 
void go()
{
    std::unique_lock  lck(mtx);
    ready = true; // 设置全局标志位为 true.
    cv.notify_all(); // 唤醒所有线程.
}
 
int main()
{
    std::thread threads[10];
    // spawn 10 threads:
    for (int i = 0; i < 10; ++i)
        threads[i] = std::thread(do_print_id, i);
 
    std::cout << "10 threads ready to race...\n";
    go(); // go!
 
  for (auto & th:threads)
        th.join();
 
    return 0;
}

运行结果为:

0 threads ready to race...
thread 1
thread 0
thread 2
thread 3
thread 4
thread 5
thread 6
thread 7
thread 8
thread 9

输出表明所有线程被唤醒,然后依旧获得了锁。

如果将go()中的cv.notify_all()改为cv.notify_one(),运行结果为:

10 threads ready to race...
thread 1

输出表明只有有一个线程被唤醒,然后该线程释放锁,这时锁已经处理非锁定状态,但是其余线程依旧处于阻塞状态。

  • 当前线程调用 wait() 后将被阻塞(此时当前线程应该获得了锁(mutex),不妨设获得锁 lck),直到另外某个线程调用 notify_* 唤醒了当前线程,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。
  • 所以,线程阻塞在condition_variable时,它是等待notify_one()或者notify_all()来唤醒,而不是等待锁可以被锁定来唤醒。线程被唤醒后,会通过轮询方式获得锁,获得锁前也一直处理运行状态,不会被再次阻塞。

参考资料


std::condition_variable
why do I need std::condition_variable?

https://blog.csdn.net/guotianqing/article/details/104017649?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-0.control&spm=1001.2101.3001.4242

https://blog.csdn.net/weixin_44517656/article/details/112096777?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_title~default-0.control&spm=1001.2101.3001.4242

https://blog.csdn.net/qq_38210354/article/details/107168532?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EOPENSEARCH%7Edefault-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EOPENSEARCH%7Edefault-1.control

你可能感兴趣的:(c++)