28.线程互斥与同步(二)
POSIX信号量
信号量本质是一个计数器,是对特定资源的预定机制!
多线程使用资源,有两种情况:
1.将目标资源整体使用【mutex+2元信号量】
2.将目标资源按照不同的“块”,分批使用【信号量】
所有的线程,都得看到同一份资源sem,信号量本身也是临界资源。
信号量的P:--,原子的;V:++,原子的
2.CP&基于环形队列的生产者消费者模型
回顾数据结构环形队列:
逻辑上,环形队列是个圆,大小固定。
对于空和满的判定:
方案一:int count; 记录当前数据的数量
方案二:空一格,head和tail相等为空,tail下一个是head为满。
实现方式:
可以用一个长度固定的数组实现,下标每次++后就 %= 长度,就可以实现回绕。
具体实现:
约定1:空,生产者先运行
约定2:满,消费者先运行
约定3:生产者不能套消费者一圈以上
约定4:消费者不能超过生产者
思考:
1.只要消费者和生产者不在同一位置,就互不干扰,可以并发执行。
2.什么时候消费者和生产者在同一位置?空或满的时候,只要不为空或满就可以同时运行。
3.为空,只能(互斥)生产者先执行(同步,顺序固定)
为满,只能(互斥)消费者先执行(同步,顺序固定)
因此,当前思路就保证了生产者和消费者之间的:互斥(只能有一个) 和 同步(固定先后)
对于生产者来说,空位置是资源;对于消费者来说,数据是资源
伪代码呈现思路:
生产者 消费者
初始(sem_blank = N, p_step = 0) (sem_data = 0, c_step = 0 )
P(sem_blank); // 空位置-- P(sem_data);
在p_step的位置生产 在c_step的位置消费
p_step++; c_step++;
p_step %= N; // 保持环形队列 c_step %= N;
V(sem_data); // 数据++ V(sem_blank);
注:信号量P操作是原子的,申请成功,继续运行,申请失败,申请的线程会被阻塞。
分析整个流程,各个情况,以单生产者,单消费者来看:
情况1:sem_blank为N,sem_data为0,为空的情况
生产者线程P操作成功,消费者P操作会阻塞,直到生产者V操作把sem_data++。
情况2:sem_blank为0,sem_data为N,为满的情况
生产者P操作阻塞,消费者P操作成功,直到消费者V操作把sem_blank++。
情况3:sem_blank为x, sem_data为N-x,x>0,其他情况
两者P操作都可以成功,互不干扰,可以并发执行
总结:
环形队列用信号量来实现生产者消费者模型的原因:环形队列的长度固定,资源明确划分,资源数量确定。生产者和消费者在进行同一块资源访问时,必有一个信号量为0,要阻塞,等待另一个V操作++,这样就保证了同步和互斥;访问不同块资源时,没有任何限制,可以并发执行。
3.熟悉信号量的接口,编写代码
#include <semaphore.h>
sem_t sem;
初始化:
sem_init(&sem, 0, value); //第二个参数0表示线程间共享,非0表示进程间共享;第三个参数value表示信号量的初始值
销毁:
sem_destroy(&sem);
等待信号量(P操作,--):
sem_wait(&sem);
发布信号量(V操作,++):
sem_post(&sem);
信号量的封装:
#pragma once #include <semaphore.h>class Sem { public:Sem(unsigned int num){sem_init(&_sem, 0, num);}~Sem(){sem_destroy(&_sem);}void P(){sem_wait(&_sem);}void V(){sem_post(&_sem);}private:sem_t _sem; };
RingQueue.hpp实现:
#pragma once#include "Sem.hpp" #include <vector> #include "Mutex.hpp"using namespace MutexModule; static const unsigned int defaultnum = 5;template<typename T> class RingQueue { public:RingQueue(unsigned int cap = defaultnum): _v(cap, 0), _cap(cap), _pstep(0), _cstep(0), _sem_blank(cap), _sem_data(0){}~RingQueue(){}void EnQueue(const T &data){// 生产者_sem_blank.P();{// 先竞争信号量,再竞争锁,效率更高MutexGuard mutexguard(_pmutex);// 1.执行_pstep位置的任务_v[_pstep] = data;// 2.迭代_pstep++;// 3.维持环形队列结构_pstep %= _cap;}_sem_data.V();}void Pop(T *data){// 消费者_sem_data.P();{MutexGuard mutexguard(_cmutex);*data = _v[_cstep];_cstep++;_cstep %= _cap;}_sem_blank.V();}private:std::vector<T> _v; // 数组模拟环形队列int _cap; // 环形队列容量int _pstep; // 生产者下标int _cstep; // 消费者下标Sem _sem_blank; // 空位置信号量,生产者资源Sem _sem_data; // 数据信号量,消费者资源Mutex _pmutex; // 生产者间的互斥锁Mutex _cmutex; // 消费者间的互斥锁 };
注意的点:信号量只能保证生产者和消费者之间的互斥和同步关系。
为了实现多生产者和多消费者,需要实现生产者间的互斥关系,消费者间的互斥关系。
锁的申请要在申请信号量之后,原因:
1)信号量的PV操作是原子的,不用锁保护
2)先竞争信号量,再竞争锁,效率更高 -> 先申请信号量,线程是并行执行的,可以让线程先持有资源,在申请锁成功可以直接执行临界区代码;若是先申请锁,那么每次只能有一个线程去竞争信号量,其他线程在锁释放前什么也做不了,效率低下 -> 这也是锁要尽可能细的加在临界区之间,尽量不要包含非临界区代码的原因