条件变量
背景介绍
对于一般的生产者-消费者模型,生产者会产生数据供消费者使用。生产者需要在准备好数据的时候通知消费者,消费者通过判断是否已经有数据来决定要不要消费数据。一般情况下,生产者和消费者的实现如下所示:
#include<bits/stdc++.h>
#include<mutex>
#include<unistd.h>
using namespace std;
vector<int> buffer;
mutex mtx;
bool dataIsReady = false;
void producer(){
while (true) {
sleep(2);
std::unique_lock<std::mutex> lk(mtx);
//随机生成10个数字
for (int i = 0; i < 10; i++) {
buffer.push_back(rand()%20);
}
dataIsReady = true;
}
}
void consumer(){
while (true) {
sleep(2);
std::unique_lock<std::mutex> lk(mtx);
if (!dataIsReady)
continue;
//输出10个数字之后清空数组
for (int i = 0; i < buffer.size(); i++) {
if (i) putchar(' ');
cout << buffer[i];
}
cout << endl;
buffer.clear();
dataIsReady = false;
}
}
int main(){
srand(time(NULL));
thread produc(producer);
thread consum(consumer);
produc.join();
consum.join();
return 0;
}
运行结果如下所示:
14 8 6 13 18 2 16 5 3 14
10 14 4 0 15 12 8 13 1 3
17 0 15 5 4 6 1 4 17 11
0 3 19 6 17 18 8 5 15 4
0 5 10 16 18 18 1 6 11 2
10 0 15 17 18 11 4 11 15 13
2 15 9 14 13 18 12 14 3 19
18 3 17 0 12 15 18 13 13 1
7 3 2 14 13 0 18 9 3 13
...
这种写法可以正常工作,但是存在一个问题:每次consumer线程都会循环判断数据是否准备好,这个过程中线程不会让出资源,因此循环判断会带来不必要的开销。正因如此,才需要学习条件变量(condition_variable)。
条件变量
条件变量(condition_variable)是C++中的一个类,能够阻塞线程直到相关条件满足时被唤醒。条件变量使用wait函数阻塞线程,wait函数一般与unique_lock配合使用。直到有其他线程中的同一个条件变量调用了唤醒函数,当前被wait阻塞的线程才会被唤醒。使用条件变量可以再相关条件不满足时阻塞线程以让出资源,防止因为不断的循环加锁判定带来的开销。
除了构造函数和析构函数以外,condition_variable类的成员函数有wait函数和notify函数:
wait函数
wait函数有三个,分别为wait、wait_for、wait_until,这里着重介绍一下wait方法:
wait函数有以下两个版本:
类型 | 函数原型 |
---|---|
unconditional (1) | void wait (unique_lock<mutex>& lck); |
predicate (2) | template<class Predicate> void wait (unique_lock<mutex>& lck, Predicate pred); |
(1)参数lck
一个加了锁的unique_lock对象,所有调用了当前条件变量wait方法的对象都应该在下层使用同一个mutex对象。
(2)参数pred
一个可以调用的对象或者函数,函数或者对象没有参数并且需要返回一个bool类型的值,线程将会不停的调用wait函数直到该返回值为true;
对于以上两个版本的函数,其使用效果如下所示,且以下示例等价:
std::mutex mtx;
std::condition_variable cv;
bool isReady;
//写法一
while(true) {
sleep(1);
std::unique_lock<std::mutext> lk(mtx);
while (!isReady) {
cv.wait(lk);
}
}
//写法二
while (true) {
sleep(1);
std::unique_lock<std::mutext> lk(mtx);
cv.wait(lk, []{return isReady;});
}
使用lambda函数更显简洁,因此建议第二种写法。
notify函数
notify函数有两个,分别为notify_one和notify_all。
(1)notify_one
唤醒一个满足该条件变量wait函数的线程,如果没有线程处于等待状态,那么此函数不做任何事。如果等待的线程有多个,那么随机选择一个线程唤醒。
(2)notify_all
唤醒所有满足该条件变量wait函数的线程,如果没有线程处于等待状态,那么此函数不做任何事。
notify函数的示例如下所示:
// condition_variable::notify_all
#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 print_id (int id) {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) cv.wait(lck);
// ...
std::cout << "thread " << id << '\n';
}
void go() {
//修改ready标记,并通知打印线程工作
std::unique_lock<std::mutex> lck(mtx);
ready = true;
cv.notify_all();
}
int main (){
std::thread threads[10];
// 创建10个线程,每个线程当ready标记为真时打印自己的id号
for (int i=0; i<10; ++i)
threads[i] = std::thread(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 0
thread 1
thread 2
thread 3
thread 4
thread 5
thread 6
thread 7
thread 8
thread 9
程序在调用了go()函数后,修改了ready标记,然后通知其他线程工作,其他线程便不再阻塞,完成打印工作。
条件变量的使用
熟悉了条件变量的常用变量及其使用方法,那么我们可以用来实现常见的模型,比如生产者-消费者模型。
生产者-消费者模型
生产者-消费者模型中生产者负责生产数据,消费者负责消费数据。这里使用5个生产者和5个消费者对一个buff进行操作,这个buffer使用队列存储数据,生产者生产的数据按照顺序存入,消费者在buffer不空时从队列头部取出数据。为了防止生产者产生太多的数据,这里给buffer添加了容量限制。
生产者-消费者模型的结构如下所示:
其代码实现如下所示:
#include<bits/stdc++.h>
#include<unistd.h>
#include<mutex>
#include<condition_variable>
using namespace std;
std::mutex mtx; //mutex变量
std::condition_variable cv; //condition_variable变量
std::queue<int> databuffer; //数据buffer
//队列的最大容量
const int MAXSIZE = 100;
void producer(){
while (true) {
sleep(5);
//获取锁
std::unique_lock<std::mutex> lk(mtx);
//等待,直到数据不满则生产数据
cv.wait(lk,[]{return databuffer.size() < MAXSIZE;});
int val = rand();
databuffer.push(val);
cout << "[ Producer Thread ]-Thread_id: " << std::this_thread::get_id() << " Push " << val << endl;
//生产数据通知其他线程
cv.notify_all();
}
}
void consumer(){
while (true) {
sleep(5);
//带参数的构造函数 创建之后立即加锁,销毁对象时会自动解锁,所以定义在局部使用会比较方便
std::unique_lock<std::mutex> lk(mtx);
//等待,直到数据buffer不空时消费数据
cv.wait(lk,[]{return !databuffer.empty();});
int val = databuffer.front();
databuffer.pop();
cout << "[ Consumer Thread ]-Thread_id: " << std::this_thread::get_id() << " Pop " << val << endl;
//消费数据后通知其他线程
cv.notify_all();
}
}
int main(){
srand(time(nullptr));
vector<thread> producers;
vector<thread> consumers;
//创建线程
for (int i =0 ; i < 5; i++) {
producers.push_back(thread(producer));
consumers.push_back(thread(consumer));
}
for (int i =0 ; i < 5; i++) {
producers[i].join();
consumers[i].join();
}
return 0;
}
部分运行结果如下所示:
[ Producer Thread ]-Thread_id: 140737348204288 Push 1064460442
[ Consumer Thread ]-Thread_id: 140737339811584 Pop 1064460442
[ Producer Thread ]-Thread_id: 140737331418880 Push 1572334329
[ Consumer Thread ]-Thread_id: 140737323026176 Pop 1572334329
[ Producer Thread ]-Thread_id: 140737314633472 Push 53770771
[ Consumer Thread ]-Thread_id: 140737306240768 Pop 53770771
[ Producer Thread ]-Thread_id: 140737297848064 Push 1843725020
[ Consumer Thread ]-Thread_id: 140737289455360 Pop 1843725020
[ Producer Thread ]-Thread_id: 140737281062656 Push 1810443650
[ Consumer Thread ]-Thread_id: 140737272669952 Pop 1810443650
[ Producer Thread ]-Thread_id: 140737348204288 Push 165477773
[ Consumer Thread ]-Thread_id: 140737339811584 Pop 165477773
[ Producer Thread ]-Thread_id: 140737331418880 Push 24357394
[ Producer Thread ]-Thread_id: 140737314633472 Push 2070814110
[ Consumer Thread ]-Thread_id: 140737323026176 Pop 24357394
[ Consumer Thread ]-Thread_id: 140737306240768 Pop 2070814110
[ Producer Thread ]-Thread_id: 140737297848064 Push 1358644763
...
参考博客C++11条件变量使用详解.
父母子女-水果问题
问题描述:
父亲、母亲分别向一个果盘中放置一个水果。父亲放置苹果,母亲放置橘子。儿子专门等待果盘中的苹果。女儿专门等待果盘中的橘子。当果盘中准备好水果以后,儿子和女儿分别根据自己的需要拿走水果。
这其实也是一个生产者和消费者的模型,使用条件变量实现如下:
#include<bits/stdc++.h>
#include<mutex>
#include<unistd.h>
using namespace std;
mutex plate_mtx; //盘子互斥量
condition_variable plate_cv; //条件通知
//三个条件标记
bool appleIsReady = false; //苹果已准备好
bool orangeIsReady = false; //橘子已准备好
bool plateIsEmpty = true; //盘子已经空了
void father() {
int cnt = 1;
while (true) {
sleep(1);
unique_lock<mutex> lck(plate_mtx);
plate_cv.wait(lck, []{return plateIsEmpty;});
//如果盘子不空,则准备苹果
cout << "[Father] : I prepared my " << cnt;
switch (cnt) {
case 1 : cout << "st ";break;
case 2 : cout << "nd ";break;
case 3 : cout << "rd ";break;
default: cout << "th ";break;
}
cnt++;
cout << "apple." << endl;
//修改盘子标记和苹果标记
plateIsEmpty = false;
appleIsReady = true;
plate_cv.notify_all();
}
}
void mother() {
int cnt = 1;
while (true) {
sleep(1);
unique_lock<mutex> lck(plate_mtx);
plate_cv.wait(lck, []{return plateIsEmpty;});
//如果盘子不空则准备橘子
cout << "\t[Mother] : I prepared my " << cnt;
switch (cnt) {
case 1 : cout << "st ";break;
case 2 : cout << "nd ";break;
case 3 : cout << "rd ";break;
default: cout << "th ";break;
}
cnt++;
cout << "orange." << endl;
//修改盘子标记和橘子标记
plateIsEmpty = false;
orangeIsReady = true;
plate_cv.notify_all();
}
}
void son() {
while (true) {
sleep(1);
unique_lock<mutex> lck(plate_mtx);
plate_cv.wait(lck, []{return !plateIsEmpty && appleIsReady;});
//当盘子不空且苹果已经准备好的情况下拿苹果,并修改标记
cout << "\t\t[Son] : I get an apple! Thank you, dad!" << endl;
plateIsEmpty = true;
appleIsReady = false;
plate_cv.notify_all();
}
}
void daughter() {
while (true) {
sleep(1);
unique_lock<mutex> lck(plate_mtx);
plate_cv.wait(lck, []{return !plateIsEmpty && orangeIsReady;});
//当盘子不空且橘子已经准备好的情况下拿橘子,并修改标记
cout << "\t\t\t[daughter] : I get an orange! Thank you, mom!" << endl;
plateIsEmpty = true;
orangeIsReady = false;
plate_cv.notify_all();
}
}
int main() {
//创建线程
thread father_thread(father);
thread mother_thread(mother);
thread son_thread(son);
thread daughter_thread(daughter);
//join所有线程
father_thread.join();
mother_thread.join();
son_thread.join();
daughter_thread.join();
return 0;
}
部分执行结果如下所示:
[Father] : I prepared my 1st apple.
[Son] : I get an apple! Thank you, dad!
[Mother] : I prepared my 1st orange.
[daughter] : I get an orange! Thank you, mom!
[Father] : I prepared my 2nd apple.
[Son] : I get an apple! Thank you, dad!
[Mother] : I prepared my 2nd orange.
[daughter] : I get an orange! Thank you, mom!
[Father] : I prepared my 3rd apple.
[Son] : I get an apple! Thank you, dad!
[Mother] : I prepared my 3rd orange.
[daughter] : I get an orange! Thank you, mom!
[Father] : I prepared my 4th apple.
[Son] : I get an apple! Thank you, dad!
[Mother] : I prepared my 4th orange.
[daughter] : I get an orange! Thank you, mom!
[Father] : I prepared my 5th apple.
[Son] : I get an apple! Thank you, dad!
[Mother] : I prepared my 5th orange.
[daughter] : I get an orange! Thank you, mom!
···
Comments | NOTHING