信号量
上一期讲了关于生产者消费者模型,但是在代码中仍有不足之处
pthread_mutex_lock(&_mutex);
while(is_full())
{
pthread_cond_wait(&_pcond,&_mutex);//如果生产条件不满足,无法生产,需要等待
}
_q.push(in)
pthread_cond_signal(&_ccond);
pthread_mutex_unlock(&_mutex);
对于上诉代码,一个线程在操作临界资源的时候,必须临界资源是需要满足条件的,但是对于公共资源是否满足生产或者消费条件,无法直接得知(在没有访问之前,是无法得知的),所以只能先加锁,在检测,在操作,在解锁,也就是说在操作临界资源的时候,有可能临界资源没有就绪,但是我们无法提前知道,所以只能对资源整体加锁,就默认了对资源整体使用。实际的情况可能存在:一份公共资源允许你访问不用的区域。因此引入了信号量。
什么是信号量?信号量的本质是一个计数器,只要拥有信号量,就在未来一定能够拥有资源的一部分,申请信号量的本质:对临界资源中某一块资源的预定机制。
为什么要有信号量:需要通过信号量来评估资源的使用率,在访问临界资源的之前,需要通过信号量去申请访问临界资源的资格。通过信号量的方式,我们在访问临界资源的时候,就可以提前知道临界资源的使用情况。
对于信号量而言,线程要访问临界资源中的某一区域,就需要申请信号量,而对于信号量,所有人都需要看到看到这一部分信号量,那么信号量也是公共资源。对于信号量,只要申请成功,就一定有你的资源,只要申请失败,就说明条件不就绪,只能等待。
关于信号量的一些函数:
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value); //信号量的初始化
// pshared=0 表示线程间共享,非0表示线程间不共享
// value 信号量的初始量
int sem_destroy(sem_t *sem);
//功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
//功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()
//对于信号量而言,本质是一个计数器-递增or递减
sem--; //申请 资源 --必须保证操作的原子性 --P操作
sem++; //归还资源 --必须保证操作的原子性 --V操作
为了代码编写信号量的问题,采用了环形队列
为了完成环形队列的生产消费模型,需要做的核心工作:1、消费者的下标不能超过生产者的下标;2、生产者不能把消费者套圈生产;3、最开始的时候一定是先生产,在消费。
信号量在环形队列中充当的角色就是:衡量临界资源中资源的数量。对于生产者而言,看重的是队列中剩余的空间,空间资源定义为一个信号量,对于消费者而言,看中的是放入队列中的数据,数据资源定义为一个信号量。
在环形队列中,大部分情况下,单生产和单消费是并行执行的,只有在满或者空的时候,才有互斥和同步的情况。
代码如下:
//RingQueue.h
#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
#include <cassert>
const int getcap = 5;
template <class T>
class ring_queue
{
private:
// P操作主要是对数据量进行预定
void P(sem_t &sem)
{
int n = sem_wait(&sem);
assert(n == 0);
(void)n;
}
//主要是对信号量进行还原
void V(sem_t &sem)
{
int n = sem_post(&sem);
assert(n == 0);
(void)n;
}
public:
ring_queue(const int cap = getcap) : _queue(cap), _cap(cap)
{
// 初始化信号量
int n = sem_init(&_productor_sem, 0, _cap); // 初始化信号量,注意这里对生产者数量的初始化时剩余的可以放置任务线程的个数
assert(n == 0);
(void)n;
n = sem_init(&_comsumer_sem, 0, 0); //初始化消费者生产量,注意这里是对消费者可以消费的数量进行初始化,初始应该为0
assert(n == 0);
(void)n;
//对消费者和生产者的位置进行初始化
_spacePostion = _dataPostion = 0;
//对锁进行初始化,这里需要两把锁,对于 生产者和消费者而言,他们是互斥的
pthread_mutex_init(&_productor_mutex,nullptr);
pthread_mutex_init(&_comsumer_mutex,nullptr);
}
void Push(const T &in)
{
// 主要是进行PV操作,首先需要明确的是PV操作是原子性的
//首先对资源进行预定
P(_productor_sem);
//对这里进行加锁的时候需要注意,在P操作之后加锁,在V操作之前解锁
//因为PV操作是原子性的,所以可以不用加锁,而且加锁的话会影响其余线程提前对资源的预定,浪费时间
//如果在P操作之前加锁,那么必须等这个线程在释放锁之后,其余线程才能申请资源在进行后续操作 ,比较浪费时间
//生产者消费者模型节约时间主要是在 在该生产者线程进行操作的时候,其余线程可以先去预定资源,申请任务,以及后续的在任务执行完成之后,并行完成后续的一系列操作
pthread_mutex_lock(&_productor_mutex);
_queue[_spacePostion++]=in;
_spacePostion%=_cap;
pthread_mutex_unlock(&_productor_mutex);
V(_comsumer_sem); //消费者可以消费的资源+1
}
void Pop(T *out)
{
//主要是对消费者进行PV操作,首先需要注意的是这里需要对消费者P操作,生产者V操作
//首先消费者对资源进行消费预定
P(_comsumer_sem);
pthread_mutex_lock(&_comsumer_mutex);
*out = _queue[_dataPostion++];
_dataPostion%=_cap;
pthread_mutex_unlock(&_comsumer_mutex);
V(_productor_sem); //生产者可以生产的资源+1
}
~ring_queue()
{
// 销毁信号量
sem_destroy(&_productor_sem);
sem_destroy(&_comsumer_sem);
//对锁进行销毁
pthread_mutex_destroy(&_productor_mutex);
pthread_mutex_destroy(&_comsumer_mutex);
}
private:
std::vector<T> _queue;
int _cap;
sem_t _productor_sem;
sem_t _comsumer_sem;
int _spacePostion;
int _dataPostion;
pthread_mutex_t _productor_mutex;
pthread_mutex_t _comsumer_mutex;
};
//Task.hpp
#include <iostream>
#include <functional>
#include <string>
class Task
{
public:
using func_t = std::function<int(int, int,char)>;
public:
Task() {} // 无参构造函数
Task(int x, int y, char op, func_t func) : _x(x), _y(y), _op(op), _func(func)
{
}
std::string operator()()
{
int result = _func(_x,_y,_op);
char buffer[128];
snprintf(buffer,sizeof buffer,"%d %c %d = %d",_x,_op,_y,result);
return buffer;
}
std::string Tostring()
{
char namebuffer[1024];
snprintf(namebuffer, sizeof namebuffer, "%d %c %d = ?", _x, _op, _y);
return namebuffer;
}
private:
int _x;
int _y;
char _op;
func_t _func;
};
const std::string opera = "+-*/%";
int myMath(int x, int y, char op)
{
int result;
switch (op)
{
case '+':
result = x + y;
break;
case '-':
result = x - y;
break;
case '*':
result = x * y;
break;
case '/':
if (y == 0)
{
std::cout << "DIV error" << std::endl;
result = -1;
}
result = x / y;
break;
case '%':
if (y == 0)
{
std::cout << "Mod error" << std::endl;
result = -1;
}
result = x % y;
break;
default:
break;
}
return result;
}
//main.cc
#include "RingQueue.hpp"
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#include "Task.hpp"
void *productor_routinue(void *args)
{
// //version1 单生产 单消费
// ring_queue<int> *rq = static_cast<ring_queue<int> *>(args);
// while(true)
// {
// //产生数据,放入到环形队列中
// sleep(3);
// int data = rand()%10+1;
// rq->Push(data);
// std::cout<<"生产者生产数据: "<<data<<std::endl;
// }
// return nullptr;
//version 2 多生产 多消费 并且任务改为具体的函数
ring_queue<Task> *rq = static_cast<ring_queue<Task> *>(args);
while(true)
{
int x = rand()%10+1;
int y = rand()%5+1;
char op=opera[rand()%5];
Task t(x, y, op, myMath);
rq->Push(t);
std::cout<<"生产者生产数据: "<<t.Tostring()<<std::endl;
}
return nullptr;
}
void *comsumer_routinue(void *args)
{
// ///version 1 单生产 单消费
// ring_queue<int> *rq = static_cast<ring_queue<int> *>(args);
// //开始push和pop
// while(true)
// {
// int data;
// rq->Pop(&data);
// std::cout<<"消费者消费数据 :"<<data<<std::endl;
// // sleep(1);
// }
// return nullptr;
//version 2 多生产 多消费 并且任务改为具体的函数
ring_queue<Task> *rq = static_cast<ring_queue<Task> *>(args);
while(true)
{
Task t;
rq->Pop(&t);
std::cout<<"消费者消费数据: "<<t()<<std::endl;
}
return nullptr;
}
int main()
{
//对于单生产,单消费而言,只有生产和消费之间的竞争关系
//pthread_t c, p;
//如果对于多生产和多消费而言,满足321原则,3种竞争关系:生产与生产之间,消费与消费之间,生产和消费之间的竞争和 2种角色:生产者和消费者 1种公共资源:环形队列
//此时 对于这个环形队列而言,就是一个公共资源,需要加锁,防止多个生产线程同时进去,改变n
pthread_t c[4],p[5];
srand((unsigned int)time(nullptr)^getpid()^pthread_self()); //抛下随机数种子
//ring_queue<int> *rq = new ring_queue<int>();
ring_queue<Task> *rq = new ring_queue<Task>();
// 创建消费者和生产者线程
for(int i=0;i<5;i++) pthread_create(p+i, nullptr, productor_routinue, rq);
for(int i=0;i<4;i++) pthread_create(c+i, nullptr, comsumer_routinue, rq);
for(int i=0;i<5;i++) pthread_join(p[i], nullptr);
for(int i=0;i<4;i++) pthread_join(c[i], nullptr);
delete rq;
return 0;
}
设计模式
什么是设计模式:针对一些常见的场景,给定一些对应的解决方案,这个就是设计模式
单例模式
某一个类,只有一个对象(实例),就称之为单例模式,例如在服务器开发场景中,经常需要让服务器加载很多的数据到内存,此时就需要一个单例来管理这些数据。对于单例模式需要把对应的赋值重载函数和拷贝构造函数给禁止掉。并且构造函数放入到private里面,析构函数依然是public的,只留一个构造实例的结构用于外界访问
void operator=(const ThreadPool&) = delete;
ThreadPool(const ThreadPool &) = delete;
有两种实现方式:饿汉实现方式和懒汉实现方式:
其中比如洗碗的例子:吃完饭,立刻洗碗的方式,下一次就能直接拿碗吃饭的这就是饿汉方式
吃完饭,立刻把碗洗了,下一顿吃饭用到了这个碗在洗碗,就是懒汉方式。懒汉最核心的思想就是“延时加载”,从而优化服务器的启动速度。