- 例如一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中在将该线程唤醒。这种情况就需要用到条件变量。
接下来我们见见条件变量的接口然后再举例子理解一下
如果你想使用一个条件变量,你首先需要定义个条件变量
条件变量也是一个数据类型,是pthread库给我们提供的
pthread_cond_t //数据类型
使用前要初始化
cond:要初始化的条件变量
attr:NULL
不用了就销毁
如果条件变量是静态或者全局的我们如下初始化,就和互斥锁一样的做法。
目前我们访问临界资源的写法,就像抢票逻辑那样
先加锁
再判断 —>生产和消费条件是否满足
最后解锁
如果条件不满足,就不要再申请,而是将自己阻塞挂起!
如何挂起?条件变量给我们提供了这样的函数
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释
同样我们知道未来这个票数大于0了,我就可以去抢了,是不是也要把我叫醒啊,所以我们条件变量必然匹配了一个唤醒函数
将在指定条件变量下等待的线程尽快唤醒
唤醒一批
唤醒一个
接下来我们举个例子理解一下条件变量
今天有很多人去面试,面试地点是一个公司的会议室。当前面试官正在面试一个人,当前能不能很多人都去这个会议室让面试官面试呢?当然不能,这个面试官是一份临界资源必须要被互斥的访问,所以一次只能有一人到房间去面试。但是这家公司组织的特别不好,当前一个人面试完了让下一个人去面试的时候,大家都说我先来的你先面我,这么多人在无序的情况对这份临界资源展开竞争了,这个面试官在里面面试他也不清楚,只能是根据就近原则。面试官再次面试完一个人,大家又是一窝蜂的说我先来的。可能面试管是一个脸盲,前一个人觉得自己面的不好,但是他是最近的所以他又去面试去了,导致整个面试就他一个人在疯狂面试!到底面试效果并不好!
后来呢,有一个非常厉害的hr,这个hr管理能力很强,hr立了一个牌子叫做等待区,
让所有等待面试的人都给我区等待区去排队等,我只会按顺序的从等待区叫下一个面试的人。
此时这个等待区就是我们的条件变量
换句话说未来所有应聘者去等待面试时,都必须去条件变量下去等,面试官去唤醒某个人时,一定也是通过条件变量来唤醒等待的线程。
这个例子我们的理解是:当条件不满足的时候,我们线程必须去某些定义好的条件变量下进行等待!
接下来我们在以一张图从内核中理解这个条件变量
条件变量就是一个数据类型,假设它里面有一个状态属性,一个队列
,我们也有很多的线程
当线程申请某个资源但条件不就绪的情况下,这些线程都去队列下排队
当条件满足时,就可以唤醒等待的线程,拿到CPU上开始调度
所以我们可以认为条件变量带一个队列,条件不满足时,线程都去排队等待。
所以我们刚刚学了两个接口
下面我们先写一个简单的案例用一用条件变量
#include <iostream>
#include <pthread.h>
#include <cstdio>
#include <string>
#include <unistd.h>
int ticket = 1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
using namespace std;
void \*GetTicket(void \*args)
{
string name = static\_cast<const char \*>(args);
while (true)
{
pthread\_mutex\_lock(&lock);
// 线程进来都先排队
pthread\_cond\_wait(&cond, &lock);//为什么要有lock,后面就说
if (ticket > 0)
{
usleep(1000);
cout << name << " 正在进行抢票: " << ticket << endl;
ticket--;
pthread\_mutex\_unlock(&lock);
}
else
{
pthread\_mutex\_unlock(&lock);
break;
}
}
}
int main()
{
pthread_t t[5];
for (int i = 0; i < 5; ++i)
{
char \*name = new char[64];
snprintf(name, 64, "thread %d", i + 1);
pthread\_create(t+i, nullptr, GetTicket, name);
}
while (true)
{
sleep(1);
// 主线程一秒唤醒一个线程
pthread\_cond\_signal(&cond);
std::cout << "main thread wakeup one thread..." << std::endl;
}
for (int i = 0; i < 5; i++)
{
pthread\_join(t[i], nullptr);
}
return 0;
}
加了条件变量,我们的线程就按照一定的顺序进行访问同一份资源了。因为他们都在条件变量下排队的。
while (true)
{
sleep(1);
// 一秒唤醒一个线程
//pthread\_cond\_signal(&cond);
//唤醒一批线程
pthread\_cond\_broadcast(&cond);
std::cout << "main thread wakeup one thread..." << std::endl;
}
现在我们左手有生产者消费者模型,右手有互斥和同步,接下来我们怎么把它们结合在一起呢?
所以我们接下来写一份基于BlockingQueue的生产者消费者模型
3.基于BlockingQueue的生产者消费者模型
BlockingQueue是一个阻塞队列,首先它是一个队列,既然是一个队列就有为空的情况,同时我们对队列设定一个上限。这时当队列为满为空就要约束生产和消费应该阻塞住不应该在生产和消费了。这种我们就称之为BlockQueue。
未来生产者一定是向BlockQueue里放数据,此时BlockQueue就是一段特定结构的缓冲区,消费者一定是向BlockQueu里取数据。
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
要写生产者和消费者模式必须满足321原则,不过我们刚开始学,我们先写单生产者和单消费者维护它们之间的互斥和同步关系。后面代码写完我们在推而广之变成多生产者和多消费者。
3种关系,先写单生成,单消费
2种角色,生产者线程,消费者线程
1个交易场所,BlockQueue阻塞队列
站在编程的角度,线程A往队列中放,线程B往队列中拿。这个队列就是两个线程的共享资源。线程A放的时候线程B不能拿,线程B拿的时候线程A不能放。队列满的时候生产者线程A就不能生产了,要想办法让线程A去等待,队列空的时候消费者线程B也不能拿了,也要想办法让线程B去等待。因此我们所学互斥锁和条件变量都是需要的。
今天我们直接用C++的queue充当我们的阻塞队列。
上层调用逻辑大的框架我们先写出来
#include"BlockQueue.hpp"
void\* productor(void\* args)
{
BlockQueue<int>\* bq=static\_cast<BlockQueue<int>\*>(args);
while(true)
{
//生产活动,不断向阻塞队列中放
}
return nullptr;
}
void\* consumer(void\* args)
{
BlockQueue<int>\* bq=static\_cast<BlockQueue<int>\*>(args);
while(true)
{
//消费活动,不断向阻塞队列中取
}
return nullptr;
}
int main()
{
BlockQueue<int>\* bq=new BlockQueue<int>();
pthread_t p,c;
//两个线程看到同一个阻塞队列
pthread\_create(&p,nullptr,productor,bq);
pthread\_create(&c,nullptr,consumer,bq);
pthread\_join(p,nullptr);
pthread\_join(p,nullptr);
delete bq;
return 0;
}
阻塞队列大的逻辑框架
#pragma onec
#include<iostream>
#include<queue>
#include<pthread.h>
using namespace std;
const int maxcapacity=5;
template<class T>
class BlockQueue
{
public:
BlockQueue(const int& _capacity=maxcapacity)
:\_capacity(capacity)
{
}
//生产者放数据
void push(const T& in)//输入型参数,const &
{
}
//消费者拿数据
void pop(T\* out)//输出型参数,\* //输入输出型 &
{
}
~BlockQueue()
{
}
private:
queue<T> _q;
int _capacity;//不能让阻塞队列无限扩容,所以给一个最大容量表示队列的上限
pthread_mutex_t _mutex;//阻塞队列是一个共享资源,所以需要一把锁把它保护起来
//生产者对应的条件变量
pthread_cond_t _pcond;//队列满了,一定要让生产者在对应的条件变量下休眠
//消费者对应的条件变量
pthread_cond_t _ccond;//队列空了,让消费者也在对应条件变量下休眠
};
接下来把代码写完
#pragma onec
#include<iostream>
#include<queue>
#include<pthread.h>
using namespace std;
const int maxcapacity=5;
template<class T>
class BlockQueue
{
public:
BlockQueue(const int& capacity=maxcapacity)
:\_capacity(capacity)
{
//构造时初始化
pthread\_mutex\_init(&_mutex,nullptr);
pthread\_cond\_init(&_pcond,nullptr);
pthread\_cond\_init(&_ccond,nullptr);
}
//生产者放数据
void push(const T& in)//输入型参数,const &
{
//放之前先加锁保护共享资源,在加锁和解锁之间就是安全的临界资源
pthread\_mutex\_lock(&_mutex);
//1.判满
if(is\_full())//bug?
{
//因为生产条件不满足,无法生产,此时我们的生产者进行等待
pthread\_cond\_wait(&_pcond,&_mutex);//\_muext?
}
//2.走到这里一定是没有满
_q.push(in);
//3.绝对能保证,阻塞队列里面一定有数据
pthread\_cond\_signal(&_ccond);//唤醒消费者,这里可以有一定策略,比如说满足三分之一在唤醒
pthread\_mutex\_unlock(&_mutex);
}
//消费者拿数据
void pop(T\* out)//输出型参数,\* //输入输出型 &
{
//这里也要加锁,因为要保证访问同一份资源是安全,所以用的是同一把锁
pthread\_mutex\_lock(&_mutex);
//1.判空
if(is\_empty())//bug?
{
pthread\_cond\_wait(&_ccond,&_mutex);//\_mutex?
}
//2.走到这里我们能保证,一定不为空
\*out=_q.front();
_q.pop();
//3.绝对能保证,阻塞队列里面至少有一个空的位置
pthread\_cond\_signal(&_pcond);//这里可以有一定策略
pthread\_mutex\_unlock(&_mutex);
}
~BlockQueue()
{
//析构时销毁
pthread\_mutex\_destroy(&_mutex);
pthread\_cond\_destroy(&_pcond);
pthread\_cond\_destroy(&_ccond);
}
private:
bool is\_full()
{
return _q.size()==_capacity;
}
bool is\_empty()
{
return _q.empty();
}
private:
queue<T> _q;
int _capacity;//不能让阻塞队列无限扩容,所以给一个最大容量表示队列的上限
pthread_mutex_t _mutex;//阻塞队列是一个共享资源,所以需要一把锁把它保护起来
//生产者对应的条件变量
pthread_cond_t _pcond;//队列满了,一定要让生产者在对应的条件变量下休眠
//消费者对应的条件变量
pthread_cond_t _ccond;//队列空了,让消费者也在对应条件变量下休眠
};
#include"BlockQueue.hpp"
#include<ctime>
#include<unistd.h>
void\* productor(void\* args)
{
BlockQueue<int>\* bq=static\_cast<BlockQueue<int>\*>(args);
while(true)
{
//生产活动
int data=rand()%10+1;//在这里先用随机数.构建一个数据
bq->push(data);
cout<<"生产数据: "<<data<<endl;
sleep(1);//生产的慢一些
}
return nullptr;
}
void\* consumer(void\* args)
{
BlockQueue<int>\* bq=static\_cast<BlockQueue<int>\*>(args);
while(true)
{
//消费活动
int data;
bq->pop(&data);
cout<<"消费数据: "<<data<<endl;
}
return nullptr;
}
int main()
{
//随机数种子
srand((unsigned int)time(nullptr));
BlockQueue<int>\* bq=new BlockQueue<int>();
pthread_t p,c;
//两个线程看到同一个阻塞队列
pthread\_create(&p,nullptr,productor,bq);
pthread\_create(&c,nullptr,consumer,bq);
pthread\_join(p,nullptr);
pthread\_join(p,nullptr);
delete bq;
return 0;
}
如何证明这是一份生产者消费者模型呢?
我们先让生产者慢一点生产,生产一个消费一个
在让消费者慢一点,看到生产一堆,而消费者只能消费一个,不过消费的是历史数据,消费之后生产者才能继续生产
void\* consumer(void\* args)
{
BlockQueue<int>\* bq=static\_cast<BlockQueue<int>\*>(args);
while(true)
{
//消费活动
int data;
bq->pop(&data);
cout<<"消费数据: "<<data<<endl;
sleep(1);//消费的慢一些
}
}
代码写了测试也都通过了,但是这份代码还有很多细节需要我们雕琢的地方。
以生产为例
细节一
首先加锁然后最后才是解锁,在判断满的生产条件不满足被挂起,但是挂起的时候可是在临界区中被挂起,如果我挂起期间还持有锁,那其他线程也进不来。
因此pthread_cond_wait这个函数第二个参数,必须是我们正在使用的互斥锁!
a.该函调用的时候,会以原子性的方式将锁释放,并将自己挂起
b.该函数在被唤醒返回的时候,会自动的重新获取你传入的锁
如果当前醒来锁没有获取成功,你也必须是处于竞争锁的状态,直到获取锁成功了这个函数才会返回。换言之只要这个函数返回了这个锁一定获取成功了。
细节二
当前判断生产条件不满足就把自己挂起,但是这有个问题pthread_cond_wait这是一个函数,只要是函数就有调用失败的可能。
另外还存在伪唤醒的情况,假设只有一个消费者,十个生产者。只消费了一个但是却唤醒了一批。但是你这里是if判断,都去push肯定是有问题的。
因此充当条件判断的语法必须是while,不能用if
void push(const T& in)//输入型参数,const &
{
//放之前先加锁保护共享资源,在加锁和解锁之间就是安全的临界资源
pthread\_mutex\_lock(&_mutex);
//1.判满
while(is\_full())
{
//因为生产条件不满足,无法生产,此时我们的生产者进行等待
pthread\_cond\_wait(&_pcond,&_mutex);
}
//2.走到这里一定是没有满
_q.push(in);
//3.绝对能保证,阻塞队列里面一定有数据
pthread\_cond\_signal(&_ccond);//唤醒消费者,这里可以有一定策略,比如说满足三分之一在唤醒
pthread\_mutex\_unlock(&_mutex);
}
//消费者拿数据
void pop(T\* out)//输出型参数,\* //输入输出型 &
{
//这里也要加锁,因为要保证访问同一份资源是安全,所以用的是同一把锁
pthread\_mutex\_lock(&_mutex);
//1.判空
while(is\_empty())
{
pthread\_cond\_wait(&_ccond,&_mutex);
}
//2.走到这里我们能保证,一定不为空
\*out=_q.front();
_q.pop();
//3.绝对能保证,阻塞队列里面至少有一个空的位置
pthread\_cond\_signal(&_pcond);//这里可以有一定策略
pthread\_mutex\_unlock(&_mutex);
}
细节三
pthread_cond_signal这个函数,可以放在临界区内部,也可以放在外部。
也就是说这个唤醒可以放在解锁之前也可以放在解锁之后。但是一般建议放在里面。
因为不关心谁拿到锁,只关心有人生产消费。
下面我们修改一下代码,这样生产消费数据太low了,所以我们写了模板可以放任意内容。我们写一个任务。
//Task.hpp
#pragma once
#include <iostream>
#include <functional>
#include <string>
using namespace std;
class Task
{
typedef function<int(int, int,char)> func_t;
public:
Task(){}
Task(int x, int y, char op, func_t func) : \_x(x), \_y(y), \_op(op), \_callback(func)
{}
// 把任务返回去可以看到
string operator()()
{
int result = \_callback(_x, _y, _op);
char buffer[1024];
snprintf(buffer,sizeof buffer,"%d %c %d = %d",_x,_op,_y,result);
return buffer;
}
// 把生产的任务也打印出来
string toTaskString()
{
char buffer[1024];
snprintf(buffer,sizeof buffer,"%d %c %d = ?",_x,_op,_y);
return buffer;
}
private:
int _x;
int _y;
char _op; // 对应+-\*/%操作
func_t _callback; // 回调函数
};
string oper="+-\*/%";
// 回调函数
int mymath(int x, int y, char op)
{
int result=0;
switch (op)
{
case '+':
result = x + y;
break;
case '-':
result = x - y;
break;
case '\*':
result = x \* y;
break;
case '/':
{
if (y == 0)
{
cout << "div zero error" << endl;
result = -1;
}
else
{
result = x / y;
}
}
break;
case '%':
{
if (y == 0)
{
cout << "mod zero error" << endl;
result = -1;
}
else
{
result = x % y;
}
}
break;
default:
break;
}
return result;
}
#include"BlockQueue.hpp"
#include<ctime>
#include<unistd.h>
#include"Task.hpp"
void\* productor(void\* args)
{
BlockQueue<Task>\* bq=static\_cast<BlockQueue<Task>\*>(args);
while(true)
{
//生产活动
int x=rand()%10+1;
int y=rand()%5;
char op=oper[rand()%oper.size()];
Task t(x,y,op,mymath);
bq->push(t);
cout<<"生产任务: "<<t.toTaskString()<<endl;
sleep(1);//生产的慢一些
}
}
void\* consumer(void\* args)
{
BlockQueue<Task>\* bq=static\_cast<BlockQueue<Task>\*>(args);
while(true)
{
Task t;
bq->pop(&t);
cout<<"消费任务: "<<t()<<endl;
}
}
int main()
{
//随机数种子
srand((unsigned int)time(nullptr));
BlockQueue<Task>\* bq=new BlockQueue<Task>();
pthread_t p,c;
//两个线程看到同一个阻塞队列
pthread\_create(&p,nullptr,productor,bq);
pthread\_create(&c,nullptr,consumer,bq);
pthread\_join(p,nullptr);
pthread\_join(p,nullptr);
delete bq;
return 0;
}
现在我还想把需求变一变,我让一个线程来生产派发任务,另一个线程来消费处理任务,再来一个线程记录任务结果,将结果记录在文件中!该怎么办呢?
再来一个阻塞队列!
#pragma once
#include <iostream>
#include <functional>
#include <string>
using namespace std;
class CallTask
{
typedef function<int(int, int, char)> func_t;
public:
CallTask() {}
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以点击这里获取!](https://bbs.csdn.net/topics/618542503)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**