1. 什么是信号量
共享资源由于原子性的原则,任何时刻都只有一个执行流在进行访问。表现为互斥,也就代表共享资源起始是被当做整体来访问的。
那如果有一个共享资源,不当成一个整体,让不同的执行流访问不同的资源区域代码,只有访问同一个区域的时候我们再进行同步或者互斥,这就做到了并发。
两个问题:a. 如何知道一共有几个资源,还剩多少个? b.如何确定一块资源是不是给你的呢?
我们以买票为例,买票的本质就是对座位资源的预定机制。
信号量本质是一个计数器,当线程需要访问临界空间时,需要先申请信号量,申请成功信号量--(预定资源 P操作),使用完毕信号量资源 ++(释放资源 V操作)。
信号量的使用就是我们先申请一个信号量,当前执行流一定具有一个资源可以被他所使用,我们保证一定留有资源给该线程使用,但给哪一个资源,具体需要程序员结合场景自定义编码完成。
2. POSIX 信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
int sem_wait(sem_t *sem); //P()
功能:等待信号量,会将信号量的值减1 P()操作
发布信号量
int sem_post(sem_t *sem);//V()
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1 V()操作
3. 基于环形队列的单生产者单消费者模型
3.1 基本概念
开始时,生产和消费指向同一位置,此时为空。过程中可能再次遇到生产和消费位于同一位置,此时为空或者为满。生成和消费大部分时间都不会在同一位置,所以我们需要让二者不在同一位置时并发访问,在同一位置时具有互斥同步关系就可以了。
对于生产者而言,他在意的资源是空间资源,即N(队列的长度),没有空间时无法申请。所以我们在一开始时,就可以将空间资源置成N,每次申请,该值就--。一旦申请成功,我们就生产出来了一个数据资源,所以数据资源++。
对于消费者而言,他在意的是数据资源,即C(队列的元素个数),没有元素时无法申请。由于这个元素是生产者生产出来的,所以在一开始时,就可以将数据资源置成0。一开始一定申请不成功,于是消费者线程被阻塞。每次申请成功,C--,而且意味着存储这块数据的空间资源可以被释放,空间资源++。
综上,我们有两个信号量,space_sem_ 代表空间资源,data_sem_ 代表数据资源。当环形队列插入数据时,space_sem_进行p()操作,data_sem_进行v()操作。
删除数据时,space_sem_进行v()操作,data_sem_进行p()操作。
3.2 代码结构
后续贴出完整代码
环形队列逻辑
首先需要封装出来一个环形队列,ringqueue.hpp
主函数逻辑
信号量的封装
3.3 具体代码
3.3.1 testmain.cc
#include "ringqueue.hpp"
#include <cstdlib>
#include <ctime> //整随机数的
#include <sys/types.h>
#include <unistd.h>
void* consumer(void* args)
{
RingQueue<int>*rq=(RingQueue<int>*)args;
while(true)
{
int x;
//从环形队列中获取数据
rq->pop(&x);
std::cout<<"消费:"<<x<<"["<<pthread_self()<<"]"<<std::endl;
sleep(1);
}
}
void* productor(void* args)
{
RingQueue<int> *rq = (RingQueue<int> *)args;
while(true)
{
// sleep(1);
// 1. 构建数据或者任务对象 -- 一般是可以从外部来 -- 不要忽略它的时间消耗问题
int x = rand() % 100 + 1;
std::cout << "生产:" << x << " [" << pthread_self() << "]" << std::endl;
// 2. 推送到环形队列中
rq->push(x); // 完成生产的过程
}
}
int main()
{
//设置随机数种子
srand((uint64_t)time(nullptr)^getpid());
//创建两个线程
pthread_t c,p;
//创建一个环形队列
RingQueue<int>* rq=new RingQueue<int>();
//为了让两个线程看到同一个环形队列
pthread_create(&c,nullptr,consumer,(void*)rq);
pthread_create(&p,nullptr,productor,(void*)rq);
pthread_join(c,nullptr);
pthread_join(p,nullptr);
return 0;
}
3.3.2 ringqueue.hpp
#ifndef _Ring_QUEUE_HPP_
#define _Ring_QUEUE_HPP_
#include <iostream>
#include <pthread.h>
#include <vector>
#include "sem.hpp"
const int g_default_num=5;//缺省值
template<class T>
class RingQueue
{
public:
RingQueue(int default_num=g_default_num)
:ringqueue_(default_num)
,num_(default_num)
,c_step(0)
,p_step(0)
,space_sem_(default_num)
,data_sem_(0)
{}
~RingQueue()
{}
//生产者才push
void push(const T& in)
{
//我们需要使用信号量对临界资源进行保护
space_sem_.p();//申请空间
ringqueue_[p_step++]=in;
p_step%=num_;
data_sem_.v();
}
//消费者才pop
void pop(T* out)
{
data_sem_.p();
*out=ringqueue_[c_step++];
c_step%=num_;
space_sem_.v();
}
private:
std::vector<T> ringqueue_;
int num_;
int c_step;//消费者的下标
int p_step;//生产者的下标
Sem space_sem_;
Sem data_sem_;
};
#endif
3.3.3 测试结果
这里我让消费者每隔一秒消费一次,所以出现消费一个生产一个,且在消费完成前,生产会阻塞的现象。
4. 多生产多消费模型
生产者们的临界资源是什么?p_step
消费者们的临界资源是什么?c_step
我们要确保这两个资源是原子性的,所以我们需要在生成者和生产者之间,以及消费者和消费者之间加两把锁。
4.1 先申请信号量还是先申请锁?
信号量表征资源数量的多少,如果我们先申请信号量就相当于先分配好了任务。当一个线程进入了临界资源区时,其他线程可以先竞争信号量,然后只需要等待申请锁成功。
就好比先买票,然后排队入场。和先排队入场,再买票。
4.2 测试结果
5. 多生产多消费 和 信号量的意义
上述程序,虽然是多生产多消费,但只有一个在生产一个在消费,和单生产单消费没有什么区别。多生产多消费的意义是什么呢?
消费的本质:把公共空间的任务变成私有的。
将数据生产前和拿到之后的处理才是最耗费时间的。即拿任务虽然是一个一个拿的,但是我们处理任务是并发的。
信号量的意义是,可以不用进入临界区,就知道资源的使用情况,甚至可以减少临界资源内部的判断(买票是不是还有票,没有信号量就可能会造成疯狂的申请锁释放锁,但是不做事,所以资源不就位就应该挂起等待)。
信号量需要提前预设资源的情况,并且在p(),v()变化中,我们可以在外部就找到临界资源你的情况。信号量可以帮我们提前了解资源情况。