信号量
信号量的原理
多线程访问临界资源时,为了实现互斥。通常就是加锁,但是加锁就会导致并行访问转变为串行访问,减小访问的效率。
我们在访问临界资源时,既希望保护好临界资源,又希望保证是并行的去访问(效率不降低)。
我们将临界资源分区,提前为执行流分配对应的空间,只有得到分配空间的执行流才能访问资源。直到空间被归还。
- 这是信号量的基本思路。信号量就是一把计数器。只有提前申请信号量的执行流才能访问临界区。
- 信号量常见的操作是P(申请空间),V(释放空间)。
- P操作在计数器大于0时,就--计数器,如果为0,就阻塞等待。V操作将计数器++;
信号量的概念
Linux信号量(Semaphore)是一种实现线程或者进程同步和互斥的机制。本质就是一把计数器。
每个执行流必须先P操作申请信号量,执行完任务后V操作释放信号量。
信号量的PV操作是原子的!
因为信号量会被所有的线程看到,属于临界资源。因为对于临界资源要做到互斥,实现的方式就是申请信号量和释放信号量在汇编的一条指令,不会因为CPU调度破坏结果。
申请信号量失败(计数器为0)就会被阻塞等待,直到有执行流释放信号量后,线程被唤醒去获得信号量。因此在信号量的结构中还包含等待队列。
除此之外,还有标记信号量的状态等。
struct sem
{
int count; //计数器
wait_queue;//等待队列
status; //标记状态
}
信号量接口
- 初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:传入信号量;pshared:0为默认线程间共享;value为计数器的初值。
- 销毁
int sem_destroy(sem_t *sem);
- 申请信号量
int sem_wait(sem_t *sem);
申请成功返回0,失败阻塞等待返回-1
- 释放信号量
int sem_post(sem_t *sem);
- 成功返回0,失败返回-1
基于环形队列的生产消费者模型
基于环形队列的生产消费模型,是一种经典的并发设计模式。它解决了生产者和消费者互斥同步的问题。生产者负责生产数据并放到队列中,消费者负责从队列中取出数据,并且消费数据。环形队列作为缓冲区。
环形队列是一种先进先出的线性数据结构。与普通队列相比,环形队列首尾相接,是固定长度,减少内存碎片化。
环形队列是由数组模拟实现的
对于信号量的模型,p和c代表生产者和消费者。
p\c起始指向同一位置,每次p申请一个信号量p往前走一步,但不可先去c一圈
如果c滞后于p,那么c往前走,取出数据,释放信号量。但是c不得先于p。
必须遵守的俩个规则
生产者和消费者不能对同一位置进行访问。
- 因为无法确定是先放数据,还是取数据。所以同一位置的时候,必须是互斥的。
- 产生同一位置的情况:全空或者全满(全满,就不能申请信号量,不需要特殊处理)
- 生产者不能生产数据超过消费者一轮
如果超过,已有的数据没有来得急消费,就会被覆盖
- 消费者不会先于生产者
因为没有数据可供消费。
设计简单环形队列消费者生产模型
框架:
- Ringqueue的环形队列的底层是vector,成员包含size记录起始的空间;
- 还需要标记p 和c 的当前位置
- 需要空间信号量和数据信号量。分别给生产者和消费者。
template<class T>
class RingQueue
{
public:
RingQueue()
{
//
}
void Push()
{//}
void Pop()
{//}
~RingQueue()
{//}
private:
std::vector<T> _ringqueue;
int _size;
int _c_step;
int _p_step;
sem_t _space_sem;
sem_t _data_sem;
};
Push和Pop
生产者生产数据和消费者消费数据。都必须先申请信号量;
void Push(const T &in)
{
P(_space_sem);
_ringqueue[_p_step] = in;
_p_step++;
_p_step %= _size;
V(_data_sem);
}
void Pop(T *out)
{
P(_data_sem);
*out = _ringqueue[_c_step];
_c_step++;
_c_step %= _size;
V(_space_sem);
}
P和V
对于的就是让计数器++和--
P操作调用接口wait,V操作调用post
void P(sem_t &sem)
{
sem_wait(&sem);
}
void V(sem_t &sem)
{
sem_post(&sem);
}
这个模型优点在哪里?
- 实现生产和消费的解耦;生产者只需要关注生产,将生产好的数据放到队列中。消费者需要要取出数据来消费。
- 高度并发。只要当p和c位于同一位置,即生产者和消费者谁先放数据的时候,需要互斥判断。其它时候都是正常的。相比于阻塞队列并发程度更高。
- 空间利用率更高。信号量的设计,提前将空间预定。
- 生产者的生产数据,放数据,消费者的取数据,消费者的消费数据。绝大数都是并发的。
多生产对多消费
基于上面实现的单生产但消费。如果想要改成多生产多消费,必然会遇到问题。
信号量的申请是不受影响的。但是p和c的当前位置,就会因为线程竞争受到影响,必须加锁保护。
先申请信号量还是加锁?
为了能够最大实现性能,先申请信号量。
如果先申请锁,那么即使有空间也要等待一个执行流结束,再去申请锁,再申请信号量。
如果先申请信号量,那么锁一结束,就可以立马再申请锁,执行任务。效率更高。
基于环形队列的生产消费模型
#pragma once
#include <vector>
#include <semaphore.h>
#include "LockGuard.hpp"
const int defaultsize = 5;
template <class T>
class RingQueue
{
private:
void P(sem_t &sem)
{
sem_wait(&sem);
}
void V(sem_t &sem)
{
sem_post(&sem);
}
public:
RingQueue(int size = defaultsize)
: _size(size), _p_step(0), _c_step(0), _ringqueue(size)
{
pthread_mutex_init(&_p_mutex, nullptr);
pthread_mutex_init(&_c_mutex, nullptr);
sem_init(&_space_sem, 0, size);
sem_init(&_data_sem, 0, 0);
}
void Push(const T &in)
{
P(_space_sem);
{
LockGuard lockguard(&_p_mutex);
_ringqueue[_p_step] = in;
_p_step++;
_p_step %= _size;
}
V(_data_sem);
}
void Pop(T *out)
{
P(_data_sem);
{
LockGuard lockguard(&_c_mutex);
*out = _ringqueue[_c_step];
_c_step++;
_c_step %= _size;
}
V(_space_sem);
}
~RingQueue()
{
pthread_mutex_destroy(&_p_mutex);
pthread_mutex_destroy(&_c_mutex);
sem_destroy(&_space_sem);
sem_destroy(&_data_sem);
}
private:
std::vector<T> _ringqueue;
int _size;
int _c_step;
int _p_step;
pthread_mutex_t _p_mutex;
pthread_mutex_t _c_mutex;
sem_t _space_sem;
sem_t _data_sem;
};