C++ 多线程3 - condition_variable

  • C++98标准中并没有线程库的存在。
  • C++11中才提供了多线程的标准库,提供了threadmutexcondition_variableatomic等相关对象及功能功能。


1. 概述

  • #include<condition_variable>,该头文件中包含了条件变量相关的类,其中包括std::condition_variable
  • 我们可以使用条件变量condition_variable实现多个线程间的同步操作

线程同步是指线程间需要按照预定的先后次序顺序进行的行为

  • 通过条件变量实现同步操作,主要包括两个动作:
        1. 线程A、B约定一起行动,线程A 通过 wait*() 函数等待其他线程唤醒。
        2. 当时机合适,线程B 调用 notify_*() 唤醒 线程A。
  • 以上便达到了线程间同步的操作。

2. condition_variable

  • std::condition_variable 对象主要包含以下成员函数:
    在这里插入图片描述
  • 构造函数和 析构函数 没有值得注意的地方:
condition_variable();										// 1. 默认构造
condition_variable(const condition_variable&) = delete;		// 2. copy构造-删除

~condition_variable();										// 1. 默认析构

2.1 wait_*()

  • wait 是线程的等待动作,直到其它线程将其唤醒后,才会继续往下执行
void wait (unique_lock<mutex>& lck);						// 1. wait-default

template <class Predicate>
void wait(unique_lock<mutex>& lck, Predicate pred);			// 1. wait-predicate

   I. 默认版本wait()函数默认版本传入 unique_lock 对象 lck,该函数会自动调用 loc.unlock() 释放锁并阻塞当前线程,本线程释放锁使得其他的线程得以继续竞争锁。一旦当前线程获得 notify(通常是另外某个线程调用 notify_*() 唤醒了当前线程),wait() 函数此时再自动调用 lock.lock()上锁 并取消阻塞。

   II. predicate版本
     1. 当条件pred 返回 false 时才会调用wait()阻塞当前线程,否则继续执行后续代码。
     2. wait阻塞后,当前线程获取 notify 且 条件pred返回 true 时才能取消阻塞,这对于检查虚假唤醒调用特别有用。
     3. 该版本函数功能和下述语句相同:

while (!pred()) 		// 当pred()为false时,才会调用 wait()阻塞当前线程
	wait(lck);			// 获取notify后,当前线程取消阻塞,但仍需判断循环条件
						// 		1. pred() 为false, 重新调用 wait 阻塞当前线程
						//		2. pred() 为true, 跳出循环
  • 一般来说,wait 方法的操作可以分为三步:
         1. 释放锁,当前线程阻塞。
         2. 等待获取 notify
         3. 获取通知后,竞争获取锁。
  • 虚假唤醒:以为生产消费者问题为例,假设是在生产消费队列中。消费线程Anodify 唤醒,但是在线程A 还没有获得锁时,另一个速度更快的线程B由于其他原因也被唤醒并获得了锁,同时消费掉了队列中的数据。线程B释放锁,A获得了锁,而这时队列中已没有了数据。

底层原因:
   linux底层 : 在多核处理器下,pthread_cond_signal 可能会激活多于一个线程(阻塞在条件变量上的线程)。结果是,当一个线程调用pthread_cond_signal()后,多个调用 pthread_cond_wait() 或 pthread_cond_timedwait() 的线程返回。这种效应成为”虚假唤醒”(spurious wakeup)。
   虽然虚假唤醒在 pthread_cond_wait 函数中可以解决,为了发生概率很低的情况而降低边缘条件(fringe condition)效率是不值得的,纠正这个问题会降低对所有基于它的所有更高级的同步操作的并发度。所以 pthread_cond_wait 的实现上没有去解它。
  
   实际使用中,wait() 实际调用了上述底层函数来实现的,底层函数并未解决虚假唤醒这一问题,而是把这个任务交由上层应用去实现,即使用者需要定义一个循环去判断是否条件真能满足程序继续运行的需求

  • 此外,成员函数还包含wait_for()wait_until()

  • <1>. wait_for(): 当前线程阻塞至其他线程调用 nodify 唤醒 虚假唤醒发生 时间结束。上述三个条件只需一个即可使 wait 函数结束。此外:
       1. 当线程阻塞时,函数会自动释放锁。一旦时间结束 或 被nodify,函数会调用 loc.lock() 获取锁。
       2. 时间结束,版本1函数返回 cv_status::timeout,否则返回 cv_status::no_timeout

template <class Rep, class Period>
cv_status wait_for(unique_lock<mutex>& lck,					// 2. wait_for-1
	const chrono::duration<Rep,Period>& rel_time);
		
template <class Rep, class Period, class Predicate>			// 2. wait_for-2
bool wait_for(unique_lock<mutex>& lck,
	const chrono::duration<Rep,Period>& rel_time, Predicate pred);

  • <2>.wait_until(): 与 wait_for 功能类似。
template <class Clock, class Duration>
cv_status wait_until(unique_lock<mutex>& lck,			// 3. wait_until
					const chrono::time_point<Clock,Duration>& abs_time);

template <class Clock, class Duration, class Predicate>
bool wait_until(unique_lock<mutex>& lck,				// 3. wait_until
				const chrono::time_point<Clock,Duration>& abs_time,
				Predicate pred);

2.2 nodify_*()

  • 这里介绍 nodify_onenodify_all 两个函数:
void notify_one() noexcept; 		// 1. 解锁一个 该条件condition_variable 下的 wait线程
void notify_all() noexcept;			// 2. 解锁所有 该条件condition_variable 下的 wait线程
  • 当前条件condition_variable 下没有阻塞线程,则上述函数不执行任何操作。
  • 当前条件condition_variable 下有多条阻塞线程,则 notify_one 不指定线程,多条线程竞争。

3. 生产消费者问题

  • 生产者-消费者模型 是经典的多线程并发协作模型:
  • I. 生产者用于生产数据,生产一个就往共享数据区存一个,如果共享数据区已满的话,生产者就暂停生产;
  • II. 消费者用于消费数据,一个一个的从共享数据区取,如果共享数据区为空的话,消费者就暂停取数据,生产者与消费者不能直接交互。

   测试代码: 一个生产者,多个消费者

#include <iostream>
#include <deque>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <chrono>

std::queue<int> my_que;						// 存储数据的队列
int len = 5;                                // 存储的产品上限
int count_ID = 1000;                        // 生产的产品ID

std::mutex mu;
std::condition_variable cond;

void producer() {
    
     
    int data1;
    while (true) {
    
     
        if(my_que.size() < len) {
    
               // 达到存储上限,停止生产
            
            // RAII: 下一次循环 locker被释放
            std::unique_lock<mutex> locker(mu);
            my_que.push(count_ID);

            cout << "生产产品:" << count_ID++ << ", 此时库存量:" << my_que.size() << endl;
            
            cond.notify_all();              // 唤醒消费者取货
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    }
}

void consumer(int id) {
    
    
    std::cout << "消费者" << id << "来了." << endl;
    while (true) {
    
    
        
        unique_lock<mutex> locker(mu);
        
        // 1. 当队列不为空时,跳过该句
        // 2. 当队列为空,调用wait() 阻塞当前线程
        //              2.1 直到 wait()函数接收 notify 、且队列不为空时, 线程改为就绪,获取锁
        //              2.2 否则, 当前线程继续 wait()
        cond.wait(locker, [](){
    
    return !my_que.empty();});      
        /* 
        // 与上一句含义相同
        while(my_que.empty())
            cond.wait(locker); */

        int data = my_que.front();
        my_que.pop();

        cout << "消费者" << id << " 购买产品:" << data <<endl;
        std::this_thread::sleep_for(std::chrono::seconds(3));
    }
}

int main() {
    
    
    std::thread producer_container(producer);
    std::thread consumer_container[3];
    for (int i=0; i<3; ++i)
        consumer_container[i] = std::thread(consumer,i);

    producer_container.join();
    for (int i=0; i<3; ++i)
        consumer_container[i].join();

    return 0;
}

运行结果如下:
生产产品:1000, 此时库存量:1
消费者2来了.
消费者0消费者来了.
1来了.

生产产品:1001, 此时库存量:2
生产产品:1002, 此时库存量:3
生产产品:1003, 此时库存量:4
生产产品:1004, 此时库存量:5
消费者0 购买产品:1000
消费者0 购买产品:1001
消费者0 购买产品:1002
消费者0 购买产品:1003
消费者0 购买产品:1004
生产产品:1005, 此时库存量:1
生产产品:1006, 此时库存量:2
生产产品:1007, 此时库存量:3
生产产品:1008, 此时库存量:4
生产产品:1009, 此时库存量:5
消费者2 购买产品:1005
消费者2 购买产品:1006
消费者2 购买产品:1007
消费者2 购买产品:1008
消费者2 购买产品:1009
生产产品:1010, 此时库存量:1
生产产品:1011, 此时库存量:2
生产产品:1012, 此时库存量:3
生产产品:1013, 此时库存量:4
生产产品:1014, 此时库存量:5
消费者1 购买产品:1010
消费者1 购买产品:1011

  • 上述结果中加粗部分为 程序运行过程中多线程并发的结果。

猜你喜欢

转载自blog.csdn.net/u013271656/article/details/115161974