同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步;同步也可以理解为解决线程饥饿问题的一种机制
**竞态条件:**因为时序问题,而导致程序异常,我们称之为竞态条件。
理解:
条件变量就是某种临界资源所处状态的一种数据化表示 ,通过条件变量,就可知道该临界资源是否就绪
举个例子帮助理解条件变量:
例:
小王知道最新的iphone手机出来了,就打算去iphone手机店看看有没有货;于是小王就跑到手机店去问柜员,而柜员告诉他没有到货,小王很想要新出的iphone,所以小王要想知道iphone什么时候到货,很明显有俩种方法:1. 每天跑到手机店去询问柜员 2. 将自己的电话留给柜员,等货到了,就让柜员通知小王
显而易见,肯定是第二种方法更加省时省力;而第一种情况就像只有互斥锁的情况下,线程获取临界区资源状态的方法(即,每次线程都得获取互斥锁,然后进入临界区,访问临界资源,查看临界资源是否就绪,不就绪又将互斥锁释放,离开临界区);第二种方法就是,使用条件变量后的线程获取临界资源状态的方法,即线程进入临界区后,检测到条件未就绪时,就直接在对应的条件变量处进行等待(本质是将自己挂起),而当临界区资源就绪了即条件满足了,该线程就会被唤醒–而临界资源肯定不可能是自己就绪的,一定是其他线程对临界资源进行操作,使得条件满足了,而后以某种方式唤醒了等待的线程
我们再将例子扩张一下,我们知道想要iphone手机的肯定不止小王一人,所以肯定有很多人给柜台留了“电话”–(也就是会有很多线程在条件变量下等待),而柜台为了每个人都能买到iphone手机,所以就不可能在手机到货时只给一个人打电话,而是尽量给每个人打电话,让每个人都买得到iphone手机
而从上面我们大概能知晓,条件变量实现线程间同步的原理–通过条件变量,一次通知一个线程,让每个线程都能享受到临界资源,而不是让一个线程或者几个线程霸占临界资源–也即,不管线程的竞争锁能力强还是弱,先进入临界区访问临界资源的线程(竞争锁能力强),检测到临界资源状态不就绪,就会先到对应的条件变量下等待,后进入临界区的线程,也是如此;即从一开始通过竞争锁能力来争取资源,变成了集体在条件变量下等待资源,也即形成了线程间同步
注:查询临界资源状态本质上也是一种访问临界资源的行为 ,因为只有进入临界区之后,我们才能知道临界资源的状态,
条件变量底层简化理解,其实可以理解为c语言中的一个结构体
status:为1 则表示临界资源状态就绪 ,为0即表示临界资源状态为就绪(逻辑理解)
即信号变量里面实际上维护了一个等待队列的,等待该条件变量就绪的线程的控制块就会在该队列里;所以通过条件变量实现线程间同步的本质就是,线程通过在条件变量中等待资源,而不是通过竞争锁能力强获取资源
所以条件变量是有俩种行为的:1. 等待 – 即线程等待指定条件变量(实质是在该条件变量中的等待队列里) 2. 唤醒 --(条件就绪,等待队列里的线程被唤醒)
小结:
初始化和销毁
phread_cond_init
int pthread_cond_init(pthread_cond_t *__restrict__ __cond, const pthread_condattr_t *__restrict__ __cond_attr)
**作用:**初始化条件变量
参数:
cond: 条件变量名
attr: 基础属性,一般设为空即可
pthread_cond_destroy
int pthread_cond_destroy(pthread_cond_t *__cond)
**作用:**销毁信号变量
参数:
- cond:所需要销毁的信号量名
pthread_cond_wait
int pthread_cond_wait(pthread_cond_t *__restrict__ __cond, pthread_mutex_t *__restrict__ __mutex)
作用:
1.调用该函数会自动将锁释放掉,而后将自己挂起,允许其他线程抢夺锁,等待被其他线程唤醒(当条件满足时)–自动释放锁的原因是:如果不释放锁将自己挂起,就会造成死锁问题,导致其他线程一直在等不可能得到的资源
2.重新抢夺到锁之后,该函数才返回–线程本身是在临界区中被挂起的,所以醒来时就在临界区里,所以该线程不持有互斥锁显然是不合理的
参数:
- cond: 所需等待的条件变量
- mutex: 访问临界资源的互斥锁
返回值:
成功返回0,失败返回错误码
pthread_cond_signal
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *__cond)
作用: pthread_cond_signal的作用是唤醒正在对应条件变量的等待队列里的第一个线程;pthread_cond_broadcast的作用是唤醒等待队列里的所有线程 --本质都是将对应的信号量中的状态由0置1
参数
- cond: 就绪状态的条件变量
返回值:
成功返回0,失败返回错误码
例:实现一个boss控制3个员工工作的代码,员工等待老板的发号进行工作
#include
#include
#include
using namespace std;
pthread_cond_t cond;
pthread_mutex_t mtx;
#define NUM 3
void * ctlworker(void * args)
{
while(1)
{
cout<<"worker begin working"<<endl;
pthread_cond_signal(&cond);
sleep(1);
}
}
void * work(void *args)
{
int id=*(int *)args;
delete (int*)args;
while(1)
{
pthread_cond_wait(&cond,&mtx);
cout<<"worker["<<id<<"] are working ..."<<endl;
sleep(1);
}
}
int main()
{
pthread_cond_init(&cond,nullptr);
pthread_mutex_init(&mtx,nullptr);
pthread_t boss;
pthread_t worker[NUM];
pthread_create(&boss,nullptr,ctlworker,nullptr);
for(int i=0; i< NUM ; i++)
{
int* id=new int (i);
pthread_create(&worker[i],nullptr,work,(void*)id);
}
//线程等待
pthread_join(boss,nullptr);
for (int i=0 ;i< NUM ;i++)
{
pthread_join(worker[i],nullptr);
}
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mtx);
return 0;
}
运行结果:
我们发现员工工作的次序是固定的即0-2-1 ;本质上就是因为条件变量中的等待对列,而一个员工完成一次打印之后,是会继续到该队列的队尾继续进行等待的
总结:
错误示范
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
条件变量使用规范
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);
小结:
在现实生活中,我们普通消费者购买商品都是去超市和商店(或者是网上超市);而为什么不是跑到生产商品的工厂去购买商品呢? --这是人类社会的进步,早期的主流肯定是消费者和生产商直接进行交互的;而消费者和生产商直接交互的效率太低了(例如:我们现在为了买几根火腿肠然后跑到生产火腿肠的工厂去购买,这合理吗,这显然是不合理的,再者,工厂一般都分布在比较偏远的地方,消费者直接跑到工厂的成本太大),于是商店就诞生了;商店可以更高效的服务消费者,更好协调生产商和消费者之间的关系(商店收集用户需求,然后向对应的厂商拿货,并且一般的商店都会有存货,即使生产商的生产某些步骤出了问题,商店短时间内也不会受到影响,消费者短时间内的消费需求也不会受到影响);
也就是说超市实际上是社会发展的产物,是用来提高消费者和生产商之间交互的效率的…;
超市的好处:
而我们也知道,计算机上的代码实际上就是我们现实生活中的映射,计算机就是为了提高人们的生活质量而在不断发展的;
计算机世界里的生产者与消费者模型
人的角色转化到计算机世界里,可以看作是一个一个线程(CPU的最小调度单位),所以在计算机世界里的生产者和消费者实际上就是一个个线程
逻辑图理解:
转化到计算机世界的角度:
消费者的角色就由一个个消费资源的线程来充当,生产者的角色就由一个个生产“某些资源”的线程充当,而超市实际上就是磁盘中的一段内存(而内存呈现的方式不一(以不同的数据结构呈现),就形成不同场景下的生产消费模型例如:以队列的方式:堵塞队列,环形队列,以链表的方式呈现…)
而我们知道该**“超市”一定是能被所有的生产者和消费者所看到的,所以“超市”就一定是临界资源**
而我们也知道线程和线程之间是存在互斥和同步…等关系,所以计算机世界中的消费者和消费者,生产者和生产者,以及生产者和消费者之间肯定是存在一定的关系的
而将生产者与消费者模型抽象出来,其实可以使用‘321’去帮我们理解和记忆它:
- 3种关系:
- 消费者与消费者:互斥
- 生产者与生产者:互斥
- 生产者与消费者:互斥与同步
- 俩种角色:
- 生产者
- 消费者
- 一个“交易场所”(“超市”)
- 通常是以一段内存的形式的呈现
实现思路:
注:下面只展示了单生产者与单消费者模型,多生产者与多消费者模型只需增加生产和消费线程即可
运行主逻辑代码:
#include "BlockQueue.hpp"
#include
#include
#include
using namespace Lsh_CP;
void *comsumer(void *bq)
{
//comsumer producer 处必须使用指针 才能保证俩个函数访问的队列是同一对列
BlockQueue<int>* c=(BlockQueue<int>*)bq;
while(1)
{
int data;
c->Pop(&data);
std::cout<<"买家正在消费商品"<<data<<std::endl;
//sleep(1);
}
}
void *producer(void *bq)
{
BlockQueue<int>* p=(BlockQueue<int>*)bq;
while(1)
{
int good=rand()%20+1;
p->Push(good);
std::cout<<"生产者生产了商品"<<good<<std::endl;
sleep(1);
}
}
int main()
{
srand((unsigned)time(nullptr));
BlockQueue<int>* queue=new BlockQueue<int>;
pthread_t c,p;
//创建线程
pthread_create(&c,nullptr,comsumer,(void*)queue);
pthread_create(&p,nullptr,producer,(void*)queue);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
delete queue;
return 0;
}
堵塞队列实现代码:
#pragma once
#include
#include
#include
namespace Lsh_CP
{
//const T& 输入型参数 T* 输出型参数
const int de_cap=5;
template<class T>
class BlockQueue
{
private:
std::queue<T> _bq;
pthread_cond_t is_full; //容量为满时,就是消费者苏醒的信号
pthread_cond_t is_empty; //容量为空时,就是生产者苏醒工作的时候
pthread_mutex_t mutex; //互斥量,保证临界资源的原子性,
int _cap; //当前容量
private:
bool Is_full()
{
return _bq.size()==_cap;
}
bool Is_empty()
{
return _bq.size()==0;
}
void wake_producer()
{
pthread_cond_signal(&is_full);
}
void wake_comsumer()
{
pthread_cond_signal(&is_empty);
}
public:
BlockQueue(int capacity=de_cap):_cap(capacity)
{
pthread_mutex_init(&mutex,nullptr);
pthread_cond_init(&is_full,nullptr);
pthread_cond_init(&is_empty,nullptr);
}
~BlockQueue()
{
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&is_full);
pthread_cond_destroy(&is_empty);
}
void Push(const T& in)
{
//临界区
pthread_mutex_lock(&mutex);
while(Is_full()) //判断是否需要生产者生产
{
pthread_cond_wait(&is_full,&mutex);
}
_bq.push(in);
//唤醒消费者
if(_bq.size()>_cap/2) wake_comsumer();
pthread_mutex_unlock(&mutex);
}
void Pop(T *out)
{
pthread_mutex_lock(&mutex);
while(Is_empty())
{
pthread_cond_wait(&is_empty,&mutex);
}
*out=_bq.front();
_bq.pop();
//唤醒生产者
if(_bq.size()<_cap/2) wake_producer();
pthread_mutex_unlock(&mutex);
}
};
}
运行结果:
几点说明:
唤醒消费者或生产者的代码,放在临界区外和临界区内都可
控制消费者和生产者之间的协同关系,只需控制主逻辑中的comsumer和producer方法即可:
其他场景类似,只需要控制comsumer和producer中的代码逻辑即可
而其实我们之前所学进程间通信的管道其实就是典型的生产者与消费者模型;
区外和临界区内都可
放在unlock()之前,被唤醒的线程也得先竞争锁,即需要等当前线程退出临界区将锁释放后,被唤醒的线程重新竞争到锁,才会真正地在临界区中苏醒,所以不会有线程安全问题
放在unlock()之后,即当前线程是先退出临界区将锁释放后,才唤醒的线程,但被唤醒的线程依旧需要重新竞争到锁,才会真正地在临界区中苏醒,所以也不会有线程安全问题
即俩者的区别实际上就是:被唤醒的线程是在等待锁释放后去竞争锁,还是锁已经被释放了再去竞争锁;就效率而言,第一种方法可能效率会更高
控制消费者和生产者之间的协同关系,只需控制主逻辑中的comsumer和producer方法即可:
其他场景类似,只需要控制comsumer和producer中的代码逻辑即可
而其实我们之前所学进程间通信的管道其实就是典型的生产者与消费者模型;