C++笔记--条件变量

Posted ljt2724960661

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++笔记--条件变量相关的知识,希望对你有一定的参考价值。

        这一节主要学习条件变量,条件变量(Condition Variable)的一般用法是:线程 A 等待某个条件并挂起,直到线程 B 设置了这个条件,并通知条件变量,然后线程 A 被唤醒执行。它产生的原因是一般来说,希望一个线程等待特定事件的发生或是一个条件变为 true 是常见的事情。虽然通过定期检查“任务完成”的标识或是在共享数据中存储类似的东西也能够做到这一点,但却不甚理想。 对于像这样的线程间同步操作的需求 C++标准库提供了以条件变 量( condition variab les )和期值 ( future )为形式的工具来处理它。

  等待事件或其他条件

         如果一个线程正等待着第二个线程完成一项任务,它有几个选择。首先,它可以一直检查共享数据(由互斥元保护)中的标识,并且让第二个线程在完成任务时设置该标识。这有两项浪费,线程占用了宝贵的处理时间去反复检查该标识,以及当互斥元被等待的线程锁定后,就不能被任何其他线程锁定。
        等待中的线程消耗了本可以被系统中其他线程使用的资源,并且最终等待的时间可能会比所需的更长。第二个选择是使用 std: : this thread: : sleep for ()函数,让等待中的线程在检查之间休眠一会儿。

bool flag;
std::mutex m;

void wait_for_flag()

  std::unique_lock<std::mutex> lk(m);
  while(!flag)
  
    lk.unlock();  // 1 解锁互斥量
    std::this_thread::sleep_for(std::chrono::milliseconds(100));  // 2 休眠100ms
    lk.lock();   // 3 重新锁定互斥量
  

分析: 这个循环中,在休眠前2,函数对互斥量进行解锁1,并且在休眠结束后再对互斥量进行上锁,所以另外的线程就有机会获取锁并设置标识。这个实现优化很多,因为当线程休眠时,线程没有浪费执行时间,但是很难确定正确的休眠时间。太短的休眠和没有休眠一样,都会浪费执行时
间;太长的休眠时间,可能会让任务等待线程醒来。休眠时间过长是很少见的情况,因为这会直接影响到程序的行为,当在高节奏游戏中,它意味着丢帧,或在一个实时应用中超越了一个时间片。第三个选择,同时也是首选选择,是使用 C++标准库提供的工具来等待事件本身。等待由另一个线程触发一个事件的最基本机制是条件变量(condition variable).

 用条件变量等待条件

      标准 C++库提供了两个条件变量的实现: std: :condition_ variable 和std : :condition_variable_any。这两个实现都在<condition_variable>库的头文件中声明。两者都需要和互斥元一起工作,以便提供恰当的同步;前者仅限于和std: : mutex 一起工作,而后者则可以与符合成为类似互斥元的最低标准的任何东西一起工作,因此以_any 为后缀。因为 std: :condition_variab le_any 更加普遍,所以会有大小、性能或者操作系统资源方面的形式的额外代价的可能,因此应该首选std: :condition_variable ,除非需要额外的灵活性。

    如何使用 std::condition_variable 去处理怎么让正在等待工作的线程休眠,直到有数据要处理?如下:

std::mutex mut;
std::queue<data_chunk> data_queue;  // 1
std::condition_variable data_cond;

void data_preparation_thread()

  while(more_data_to_prepare())
  
    data_chunk const data=prepare_data();
    std::lock_guard<std::mutex> lk(mut);
    data_queue.push(data);  // 2
    data_cond.notify_one();  // 3
  


void data_processing_thread()

  while(true)
  
    std::unique_lock<std::mutex> lk(mut);  // 4
    data_cond.wait(
         lk,[]return !data_queue.empty(););  // 5
    data_chunk data=data_queue.front();
    data_queue.pop();
    lk.unlock();  // 6
    process(data);
    if(is_last_chunk(data))
      break;
  

分析:首先,你拥有一个用来在两个线程之间传递数据的队列1。当数据就绪时,准备数据的线程使用 std:: lock_guard 去锁定保护队列的互斥元,并且将数据压人队列中2 。然后,它在 std::condition_variable 的实例上调用 notify_one ()成员函数, 以通知等待中的线程(如果有的话)3。在另外一侧,你还有处理线程。该线程首先锁定互斥元,但是这次使用的是std: :unique_lock 而不是 std:: lock_guard 4-一你很快就会明白为什么。该线程接下来在 std: :condition_variable 上调用 wait (),传人锁对象以及表示正在等待的条件的 lambda 函数5。 lambda 函数是 C++中的一个新功能,它允许你编写一个匿名函数作为另一千表达式的一部分,它们非常适合于为类似于 wait ()这样的标准库函数指定断言。 在这个例子中,简单的 lambda 函数[] return !data_queue.empty ();检查 data_queue是否不为 empty (),即队列中已有数据准备处理。wait()会去检查这些条件(通过调用所提供的lambda函数),当条件满足(lambda函数返回true)时返回。如果条件不满足(lambda函数返回false),wait()函数将解锁互斥量,并且将这个线程(上段提到的处理数据的线程)置于阻塞或等待状态。当准备数据的线程调用notify_one()通知条件变量时,处理数据的线程从睡眠状态中苏醒,重新获取互斥锁,并且对条件再次检查,在条件满足的情况下,从wait()返回并继续持有锁。当条件不满足时,线程将对互斥量解锁,并且重新开始等待.这就是为什么用std::unique_lock而不使用std::lock_guard——等待中的线程必须在等待期间解锁互斥量,并在这这之后对互斥量再次上锁,而std::lock_guard没有这么灵活。如果互斥量在线程休眠期间保持锁住状态,准备数据的线程将无法锁住互斥量,也无法添加数据到队列中;同样的,等待线程也永远不会知道条件何时满足。

 看一个栗子:

#include <iostream>                // std::cout
#include <thread>                // std::thread
#include <mutex>                // std::mutex, std::unique_lock
#include <condition_variable>    // std::condition_variable

std::mutex mtx; // 全局互斥锁.
std::condition_variable cv; // 全局条件变量.
bool ready = false; // 全局标志位.

void do_print_id(int id)

    std::unique_lock <std::mutex> lck(mtx);
    while (!ready) // 如果标志位不为 true, 则等待...
        cv.wait(lck); // 当前线程被阻塞, 当全局标志位变为 true 之后,
    // 线程被唤醒, 继续往下执行打印线程编号id.
    std::cout << "thread " << id << '\\n';


void go()

    std::unique_lock <std::mutex> 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;

输出如下:

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

以上是关于C++笔记--条件变量的主要内容,如果未能解决你的问题,请参考以下文章

蓝桥ROS机器人之现代C++学习笔记7.4 条件变量

C++笔记--future

C++笔记--future

在 C++ 中使用互斥锁和条件变量实现带有信号的监视器

《C++ 并发编程实战 第二版》:条件变量唤醒丢失与虚假唤醒

《C++ 并发编程实战 第二版》:条件变量唤醒丢失与虚假唤醒