C++多线程——条件变量condition_variable

基本翻译std::condition_variable-cplusplus加参考std::condition_variable-cppreference其他博客。

1.condition_variable概述

condition_variable 类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable 。

当其wait function之一(waitwait_forwait_until)被调用时,其使用一个unique_lock去阻塞该线程,该线程将一直阻塞直到被另一个线程通过调用同一个condition_variable对象的 notification function 唤醒。

std::condition_variable 只可与 std::unique_lock一同使用;

condition_variable 不可复制构造 (CopyConstructible) 、可移动构造 (MoveConstructible) 、可复制赋值 (CopyAssignable) 或可移动赋值 (MoveAssignable) 。

其成员函数比较少,包括三大类:

  • 构造析构函数
  • Wait functions(wait wait_for wait_until)
  • Notify functions(notify_one notify_all)
    C++多线程——条件变量condition_variable_第1张图片

先用一个例子大致了解一下:

#include 
#include 
#include                 //mutex unique_lock
#include 

std::mutex mtx;
std::condition_variable cv;
bool ready = false;

void print_id(int id)
{
    std::unique_lock<std::mutex> lck(mtx);
    while (!ready)
    {
        cv.wait(lck);
    }
    std::cout << "thread #" << id << "\n";
}

void go()
{
    ready = true;
    cv.notify_all();
}

int main()
{
    std::thread threads[10];
    for(int i=0; i<10; i++)
        threads[i] = std::thread(print_id, i+1);
    go(); //begin race

    for(auto& th : threads){
        th.join();
    }
    return 0;
}

运行结果:
C++多线程——条件变量condition_variable_第2张图片

2.Wait functions

2.1 wait

void wait (unique_lock<mutex>& lck);
template <class Predicate>
  void wait (unique_lock<mutex>& lck, Predicate pred);

第一种方式的wait导致当前线程阻塞直至条件变量被通知,或虚假唤醒(spurious wake-up)发生。
第二种方式的wait则在不满足条件之前会一直在while循环中,第二种方式等效为:

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

pred可以是一个函数或者lambda表达式。

C++多线程——条件变量condition_variable_第3张图片
上面这段话的大意是说:
当阻塞线程时,wait函数会自动调用lck.unlock(),允许其他锁住的线程继续;
一旦被通知,就不在阻塞并调用lck.lock()获得锁,然后函数返回。(阻塞的线程释放锁,不阻塞的线程获得锁)
但是wait函数可能会引起虚假唤醒,使用者应该保证被唤醒的条件确实被满足。

如果使用了pred,该函数之后在pred返回false时阻塞,而且notification函数只有在pred变为true时才能不再阻塞该线程,这可以有效地应对虚假唤醒现象。

看一个wait使用的例子:

#include 
#include     //mutex & unique_lock
#include 
#include 
using namespace std;

mutex mtx;
condition_variable cv;
int cargo = 0;

bool pred() { return cargo != 0; }

void consumer(int n)
{
    for (size_t i = 0; i < n; i++)
    {
        unique_lock<mutex> lck(mtx);
        cv.wait(lck, pred);//阻塞直到pred满足, 即 cargo != 0, wait 会调用mtx.unlock()

        cout << cargo << "\n";
        cargo = 0;  //再将cargo置为0, 导致主线程中的while可以执行
    }
}

int main()
{
    thread consume(consumer, 10);
    for(int i=0; i<10; i++){
        while(pred()){ //不满足条件则yield, cargo != 0则阻塞,等待consumer将cargo设置为0
            this_thread::yield();
        }
        unique_lock<mutex> lck(mtx);//调用mtx.lock()
        cargo = i+1;
        cv.notify_one();
    }
    consume.join();
    return 0;
}

在main函数中,使得cargo累加,导致pred返回true,然后consumer函数中的wait不再阻塞,输出cargo,然后consumer函数将cargo再次设置为0,进行下次循环。
输出结果:
C++多线程——条件变量condition_variable_第4张图片
刚开始时一直比较懵,main函数中没有加unique_lock lck(mtx);//调用mtx.lock()这句话;
这就导致在consumer函数中,先执行unique_lock lck(mtx);调用mtx.lock();,该线程获得锁mtx
然后执行cv.wait(lck, pred);调用lck.unlock()(相当于调用mtx.unlock())使得锁mtx被释放。
然后,for循环结束,lck被析构,再次执行lck.unlock();,相当于又一次调用了mtx.unlock()mtx被unlock了两次,这种行为是未定义的,然后程序卡死了

在main函数中加入unique_lock lck(mtx);就可以了。

假设main函数中的unique_lock lck(mtx)先获得锁,那么consumer中的unique_lock lck(mtx);就会阻塞,直到main函数运行完cv.notify_one();。main中的lck析构,mtx被释放,consumer得以执行。

假设consumer中的unique_lock lck(mtx);先获得锁,main函数中的unique_lock lck(mtx);就会阻塞,当consumer执行完wait函数后,锁被释放,main函数得以执行,但是由于consumer中wait的条件pred不满足,导致consumer阻塞直到main函数将cargo的值设置为非0.

2.2 wait_until

template <class Clock, class Duration>
  cv_status wait_until (unique_lock<mutex>& lck,
                        const chrono::time_point<Clock,Duration>& abs_time);

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

wait_until 导致当前线程阻塞直至通知条件变量、抵达指定时间或虚假唤醒发生,如果调用的是包含Predicate pred的函数,还需要等到条件满足。

包含Predicate pred的wait_until相当于:

while (!pred())
  if ( wait_until(lck,abs_time) == cv_status::timeout)
    return pred();
return true;

返回值说明:

  • 当调用的是不包含Predicate pred的wait_until时,返回一个cv_status 类型的对象,时间到则返回cv_status::timeout,否则返回cv_status::no_timeout
    cv_status是枚举类型:enum class cv_status { no_timeout, timeout };

  • 当调用的是包含Predicate pred的wait_until时,返回pred(),是一个bool。

2.3 wait_for

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

第二个参数是设置的一个时间,第三个参数是需要满足的条件。
该函数直到被notify或者时间到的时候才会返回。
执行该函数会调用lck.unlock(),被通知或者时间到时会执行lck.lock();
第二种predicate ,相当于执行:

return wait_until (lck, chrono::steady_clock::now() + rel_time, std::move(pred));

例子:

:cin >> value;
    cv.notify_one();
}

int main()
{
    std::cout << "please input value, i will output . \n"; 
    std::thread th(read_value);
    std::mutex mtx;

运行结果:
C++多线程——条件变量condition_variable_第5张图片

3. Notify functions

void notify_one() noexcept;
void notify_all() noexcept;

If no threads are waiting, the function does nothing.

4.生产者消费者

代码来自:C++11条件变量使用详解

#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;

mutex mtx;
condition_variable cv;
deque<int> datadeque;   //缓冲区
int gval = 0;           //数据

const int BUFFER_SIZE = 30;//缓冲区大小
const int PRODUCER_NUM = 3;//生产者线程数量
const int CONSUMER_NUM = 3;//消费者线程数量

void producer(int id)
{
    while (true)
    {
        this_thread::sleep_for(chrono::milliseconds(100));
        unique_lock<mutex> lck(mtx);
        cv.wait(lck, [](){ return datadeque.size() <= BUFFER_SIZE; });//当队列的大小达到BUFFER_SIZE时阻塞
        
        gval++;
        datadeque.push_back(gval);
        cout << "producer thread " << id << " produced data " << gval << ", deque size " << datadeque.size() << endl;
        cv.notify_all();
    }    
}

void consumer(int id)
{
    while (true)
    {
        this_thread::sleep_for(chrono::milliseconds(100));
        unique_lock<mutex> lck(mtx);//lock
        cv.wait(lck, [](){ return !datadeque.empty(); } );//当缓冲队列为空时阻塞 unlock

        int front = datadeque.front();
        datadeque.pop_front();
        cout << "\tconsumer thread " << id << " consumed data " << front << ", deque size " << datadeque.size() << endl;
        cv.notify_all();
    }
    
}

int main()
{
    thread pro_th[PRODUCER_NUM];
    thread con_th[CONSUMER_NUM];
    //生产者线程
    for(int i=0; i<PRODUCER_NUM; i++)
        pro_th[i] = thread(producer, i+1);
    for(int i=0; i<CONSUMER_NUM; i++)
        con_th[i] = thread(consumer, i+1);
    //消费者线程
    for(int i=0; i<PRODUCER_NUM; i++)
        pro_th[i].join();
    for(int i=0; i<CONSUMER_NUM; i++)
        con_th[i].join();
    
    return 0;
}

运行结果:
C++多线程——条件变量condition_variable_第6张图片

5.虚假唤醒

条件变量的虚假唤醒(spurious wakeups)问题
https://www.jianshu.com/p/0eff666a4875

6. C中的条件变量

C++多线程——条件变量condition_variable_第7张图片
C++多线程——条件变量condition_variable_第8张图片
C++多线程——条件变量condition_variable_第9张图片

其他

  • 互斥量
  • 原子操作
  • 读写锁
  • unique_lock

你可能感兴趣的:(操作系统,C++STL)