"于是我坐上颠坡的列车,去到那条小巷。"
int ticket = 10000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* GetTicket(void* args)
{
std::string message = (const char*)args;
while(true)
{
if(ticket > 0){
usleep(100); // 促使线程高频切换
std::cout << message << "抢到了票: " << ticket << std::endl;
ticket--;
}
else{
break;
}
}
}
int main()
{
pthread_t t1,t2,t3,t4;
pthread_create(&t1,nullptr,GetTicket,(void*)"thread 1");
pthread_create(&t2,nullptr,GetTicket,(void*)"thread 2");
pthread_create(&t3,nullptr,GetTicket,(void*)"thread 3");
pthread_create(&t4,nullptr,GetTicket,(void*)"thread 4");
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
pthread_join(t4,nullptr);
return 0;
}
此时我们想做一个抢票系统,即让每一个线程都会去访问一个全局变量ticket。
这里也就不卖关子了。问题出在,对全局数据(共享资源)的不安全访问。
ticket作为一个全局变量,它是能被进程内的所有执行流(线程)访问的,而对于ticket的--或者++,其操作不是原子的!何为原子?即"做成"或"做不成"。当一个线程流对ticket进行++或--时,翻译成汇编语句至少有三条:
而这样导致的结果,换句话说,也就是造成了数据读取的"不一致性"的问题。
如何解决数据读取的"不一致性"问题呢?也就引入了"锁"这个概念。
此时,我们在启动我们的"抢票"系统时,就不会出现不合理的数字了。不过眼细的你一定发现了数据打印的一些端倪。
为什么所有的票都由"thread 3"抢了呢?其他线程呢?我们的结论是,其他线程一定是因为没有竞争到锁,在获取锁处被阻塞挂起了!
这合乎我们的预期吗?我们的预期是什么? 不让票出现异常的数字。那么"锁"机制的引入解决问题了嘛?是的! 锁机制能够保证线程互斥地访问共享资源。可是这合理吗? 票都让一个线程"抢"了,其他线程不就都"喝西北风"了嘛?
我们为什么要引入锁机制? 你一定是为了保护临界资源。 为什么临界资源需要保护呢? 是因为多个线程访问资源时,可能会导致数据不一致性的问题。 而在多线程下,大部分的资源都是共享资源,都是被线程访问的。
所以,我们不难发现一个道理。
"任何技术都有自己的边界,技术是解决问题的,但是解决问题的同时,又会引起新的问题"
---前言
在解释这个模型之前, 可以大概设想一下,一个场景。
你是一个热爱火腿肠的男人,最钟爱的火腿肠品牌是"双汇"。
如果你想要吃火腿肠,你需要做的是:
1.找到"双汇"火腿肠加工厂。
2.并告知厂长你要一根火腿肠。
3.等待该加工厂生产,并最后拿到火腿肠。
我们从上述的图片中,显然能感觉到几点不妥的地方。
对于你而言:
1. 我想要一个火腿肠,用得了跑几十公里去厂里面嘛?
2. 我告知加工厂老板 要一根火腿唔,他也同意,并且启动机器给我制造,然而我却在这个期间什么也做不了,能做的就是 站在加工厂里 等待生产,等待拿到火腿肠。
对于厂商:
1.仅仅依靠消费者到厂索要一根火腿,我就立马开工,制造一根火腿把并给他。我卖出去这一个根火腿,竟让我倒贴钱在里面,因为机器的维修、启动关闭显然不是卖一根火腿获取的利润就可以填补的。
2.消费者来我才生产,他不来呢?或者说他来得少呢?那我就不生成了 呢??
既是不这样分析,我想在现实生活中你也不会见到这样离谱的交易场面。
而我们日常生活中的行为大抵是,想吃"火腿肠"了,找一个近邻的超市,并在它的货架上找到"双汇"牌火腿,有我就拿,没有,我还可以选择其他品牌的火腿。并且,超市也不是为你一个人开的,一定也有其他和你有同样需求的消费者进入超市,选购心仪的"火腿肠"。
上述,仅仅是站在我们日常生活的角度来看,如果我们站立在计算机的视角上来呢?
1.如果现如今没有任何火腿肠需求,厂商我自己就停止生产了嘛? 不需要!
如果我急切需要火腿,我还需要亲自去联系厂商制造火腿嘛? 不需要!
从而实现了,生产过程与消费过程的解耦。你消费你的,我生产我的,互不干扰。
2. 如果我生产多了你来不及消费怎么半?超市提供保存多余货物的货架
如果此时我想要很多根火腿肠而又来不及生产又怎么办? 不怎么办! 超市有存货。
"超市"提供了保存产品的临时场所,用计算机术语,这被称作: 缓冲区。
生产者与生产者: 竞争(互斥)关系。
消费者可能会买你的产品,也可能会买你竞争对手的产品。
消费者与消费者: 竞争(互斥)关系。
"双汇"牌子火腿肠杠杠得 好消费者都知道,但生产有限,而你又致力于购买这个品牌
生产者与消费者: 互斥 、 同步关系。
当货架已经塞满火腿时,你不能再生产(你该来消费啦~)。当货架已经没有火腿是,你不能在进行消费(你该来生产啦~)。
生产线程 与 消费线程
一段"特定结构"的缓冲区
这个也就是"321"原则,我们想写一份生产消费者模型的代码,本质上就是去维护这个原则。
1.生产过程与消费过程的解耦。
2.支持生产和消费忙闲不均。
3.支持并发,提高效率。 (现在就这样理解)
要写一个生产消费者模型的代码,无外乎维护"321"原则。
#define DEFAULT_CAP 5
template
class BlockQueue
{
public:
BlockQueue(int cap = DEFAULT_CAP)
: _cap(cap){ Init(); }
~BlockQueue(){ Destory(); }
bool Is_Full()
{
return _blockqueue.size() == size;
}
bool Is_Empty()
{
return _blockqueue.empty();
}
private:
void Init()
{
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&p_cond, nullptr);
pthread_cond_init(&c_cond, nullptr);
}
void Destory()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&p_cond);
pthread_cond_destroy(&c_cond);
}
private:
// 1.特殊结构的缓冲区
std::queue _blockqueue;
int _cap; // 类似货架 容量
// 2.三种关系
pthread_cond_t p_cond; // 生产者 (同步)
pthread_cond_t c_cond; // 消费者 (同步)
pthread_mutex_t _lock; // 互斥锁 (互斥)
};
我们完成对这个缓冲区结构的初始化。
void Push(const T& in)
{
// 多个线程都可能Push 但是 将"产品放货架"的操作 需要互斥属性
pthread_mutex_lock(&_lock);
// 如果"货架满了" 就不能继续生产了
// if(Is_Full())
while(Is_Full()){
pthread_cond_wait(&p_cond,&_lock);
}
// 走到这里说明 是一个线程,可以生产放数据
_blockqueue.push(in);
// 如果"货架" 就意味着 消费者可以来消费啦
pthread_cond_signal(&c_cond);
// 先唤醒 或者 先进行释放锁 这不影响的
pthread_mutex_unlock(&_lock);
}
void Pop(T& out)
{
// 多个线程都可能Pop 但是 将从"货架拿产品"的操作 也需要互斥属性
pthread_mutex_lock(&_lock);
// 如果"货架"根本没货 还需要能消费吗?
// if(Is_Empty())
while(Is_Empty()){
pthread_cond_wait(&c_cond,&_lock);
}
// 走到这里说明 货架有货 可以进行消费啦
out = _blockqueue.front();
_blockqueue.pop();
// 该货架的产品被取走 可以唤醒 生产者进行生产
pthread_cond_signal(&p_cond);
pthread_mutex_unlock(&_lock);
}
也许你会疑问,对这个条件变量判断的条件,是否可以是if?为什么必须是while?
我们用生产者消费者模型,拟作一个简易的计算器任务。即,生产者仅仅生产任务,消费者从队列中拿到数据,并调用任务处理的函数,完成结果运算返回。
这是一个任务队列,里面包含任务的构造,以及任务的回调函数。
#ifndef __TASK_HPP__
#define __TASK_HPP__
#include
#include
#include
#include
#include
class Task
{
public:
Task(int x,int y,char op)
:_x(x),_y(y),_op(op)
{
_func =
{
{'+',[&](){ return _x + _y; }},
{'-',[&](){ return _x - _y; }},
{'*',[&](){ return _x * _y; }},
{'/',[&](){ return _x / _y; }},
{'%',[&](){ return _x % _y; }}
};
}
std::string operator()()
{
int result = _func[_op]();
char buffer[64];
snprintf(buffer,sizeof buffer,"%d %c %d = %d",_x,_op,_y,result);
// 移动构造
return buffer;
}
private:
int _x;
int _y;
char _op;
std::unordered_map> _func;
};
#endif
最后也就完成了一个线程生产,一个线程消费,两个线程互不干扰。
唔,你说的生产者消费者模型我大概好像懂了,至于实现也大致有一些启发。但是我实在不知道为什么生产者消费者模型的优点在哪里?即便你说它能提高效率,我不知道它在哪里提高的效率。
你说并发,但其实不管是生产者还是消费者,一旦进入这个"缓冲区"时,都需要进行加锁解锁的,从而让并发的执行流,变成了串行的执行流。是的,为了保证"321"原则,我们不得不在执行流进入"缓冲区"时,进行加锁。
然而不管是生产任务、还是执行任务,这些行为都是在加锁之前的进行的!也就是说,这些任务的产生与这些任务的处理是并发的,只是将任务Push 、 Pop时是串行的。
一个执行流生产任务,影响其他执行流将其他任务Push进队列嘛? 肯定不影响!
一个执行流执行任务,影响其他执行流从该队列中Pop其他任务吗?肯定不影响!
因此,生产者消费者模型的高效并发,不是体现在其"缓冲区"上,而是体现在进入缓冲区队列前和出了缓冲区队列后!
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。
本篇到此结束,感谢你的阅读。
祝你好运,向阳而生~