信号量
前面我们已经说过,多线程之间自己不具备同步与互斥的功能,所以在多线程访问临界资源过程中,需要去限制线程,使之是安全的,前面我们提到了互斥的实现–互斥锁,同步的实现–条件变量,今天我们接着谈一个 信号量
一、什么是信号量
信号量本质是一个计数器和一个队列pcb等待队列,通过其实现进程间或者线程间的同步与互斥,关于信号量的同步与互斥的实现,我在博客 进程间通信(下) 中谈到过,虽然当时讲到的是多进程,不过与多线程类似,大家可以自行体会,在这里就不多说了。
二、相关接口
2.1 定义信号量
sem_t
2.2 初始化信号量
接口:int sem_init(sem_t * sem, int pshared, unsigned int value);
返回值:成功返回0,失败返回-1。
2.3 P操作
接口1:int sem_wait(sem_t * sem);
功能:对应信号量计数大于0时,可以访问并且计数-1,计数小于等于0时,不能访问,并且线程陷入等待。
接口2:int sem_trywait(sem_t * sem);
功能:对应信号量计数大于0时,可以访问并且计数-1,计数小于等于0时,不能访问,并且线程报错返回。
接口3:int sem_timedwait(sem_t * sem, const struct timespec * abs_timeout);
功能:在指定时间内,对应信号量计数大于0时,可以访问并且计数-1,计数小于等于0时,不能访问,并且线程陷入等待,超过时间,还是不能访问,则报错返回。
返回值:成功返回0,失败返回-1。
错误原因:需要通过errno确定
常见的几个错误原因:
EINTR(针对所有等 待接口)–表示阻塞被信号打断。
ETIMEDOUT(只针对sem_timedwait接口)–表示sem_timedwait接口等到超时。
EAGAIN(只针对sem_trywait接口)–表示非阻塞情况下,计数为0。
2.4 V操作
接口:int sem_post(sem_t * sem);
功能:计数+1,如果当前有等待的进程,则唤醒一个等待的进程。
返回值:成功返回0,失败返回-1。
2.5 信号量的销毁
接口:int sem_destroy(sem_t * sem);
功能:销毁一个信号量。
返回值:成功返回0,失败返回-1。
三、信号量的应用–环形队列
上面我们谈到了什么是信号量,以及信号量的相关接口,下面我们就应用它的本质以及它的相关接口去实现一个环形队列,来保障线程安全。
3.1环形队列的属性
class RingQueue{
private:
vector<int> _list; //数组,相当于队列
int _cap; //队列容量
int _read; //用于消费者读的下标
int _write; //用于生产者写的下标
sem_t _lock; //用于实现互斥
//用于实现同步
sem_t _pro; //生产者,用于空闲计数
sem_t _con; //消费者,用于数据计数
};
这个环形队列类似于我们C++中的循环队列,但是他还包括实现线程间互斥和同步的相关信号量。
3.2 初始化
//初始化
RingQueue(int cap)
:_list(cap)
,_cap(cap)
,_read(0)
,_write(0)
{
sem_init(&_lock,0,1); //用于互斥,所以资源为1
sem_init(&_pro,0,cap);
sem_init(&_con,0,0);
}
初始化需要徐将队列的容量给出,并且读写的其起始位置都是0,这时对于生产者(对空闲计数)而言,可用容量给定计数大小,对于消费者(对资源进行计数)而言,当前没有数据,所以计数为0,而通过给定一个信号量的计数为1来实现互斥。
3.3 出队与入队
//入队
bool push(const int data){
sem_wait(&_pro);
sem_wait(&_lock);
_list[_write]=data;
_write=(_write+1)%_cap;
sem_post(&_lock);
sem_post(&_con);
return true;
}
//出队
bool pop(int *buf){
sem_wait(&_con);
sem_wait(&_lock);
*buf=_list[_read];
_read=(_read+1)%_cap;
sem_post(&_lock);
sem_post(&_pro);
return true;
}
这里只需要注意,在数据生产或者处理过程中需要加锁处理,这里就通过信号量的计数最大为1,来实现互斥,并且是环形队列,所以需要去进行循环处理,也就是每次下标移动需要去模上容量,放置越界,而当数据时,消费之阻塞等待,当数据满了时,生产者阻塞等待,这里的阻塞等待也是通过对应的信号量通过计数实现的,并且在每次处理完一个数据后,需要去唤醒一个等待中的生产者,每次生产完一个数据后,需要去唤醒一个等待中的消费者,实现数据的轮询生产处理。
3.4 销毁信号量
~RingQueue(){
sem_destroy(&_lock);
sem_destroy(&_pro);
sem_destroy(&_con);
}
这里没有什么多说的,就是销毁用户自己定义的信号量。
3.5 环形队列实现具体代码
#include<iostream>
#include<vector>
#include<pthread.h>
#include <semaphore.h>
using namespace std;
class RingQueue{
private:
vector<int> _list; //数组,相当于队列
int _cap; //队列容量
int _read; //用于消费者读的下标
int _write; //用于生产者写的下标
sem_t _lock; //用于实现互斥
//用于实现同步
sem_t _pro; //生产者,用于空闲计数
sem_t _con; //消费者,用于数据计数
public:
//初始化
RingQueue(int cap)
:_list(cap)
, _cap(cap)
, _read(0)
, _write(0)
{
sem_init(&_lock, 0, 1); //用于互斥,所以资源为1
sem_init(&_pro, 0, cap);
sem_init(&_con, 0, 0);
}
//入队
bool push(const int data){
sem_wait(&_pro);
sem_wait(&_lock);
_list[_write] = data;
_write = (_write + 1) % _cap;
sem_post(&_lock);
sem_post(&_con);
return true;
}
//出队
bool pop(int *buf){
sem_wait(&_con);
sem_wait(&_lock);
*buf = _list[_read];
_read = (_read + 1) % _cap;
sem_post(&_lock);
sem_post(&_pro);
return true;
}
~RingQueue(){
sem_destroy(&_lock);
sem_destroy(&_pro);
sem_destroy(&_con);
}
};
void *cus_fun(void* arg){
RingQueue *Queue = (RingQueue*)arg;
while (1){
int data;
Queue->pop(&data);
printf("%p thread put data:%d\n", pthread_self(), data);
}
}
void *pro_fun(void *arg){
RingQueue* Queue = (RingQueue*)arg;
int i = 0;
while (1){
Queue->push(i);
printf("%p thread get data:%d\n", pthread_self(), i++);
}
}
int main()
{
pthread_t ctid[3], ptid[3];
int ret;
RingQueue Queue(5);
for (int i = 0; i < 3; i++){
ret = pthread_create(&ctid[i], NULL, cus_fun, (void*)&Queue);
if (ret != 0){
printf("create cus error\n");
return -1;
}
}
for (int i = 0; i < 3; i++){
ret = pthread_create(&ptid[i], NULL, pro_fun, (void*)&Queue);
if (ret != 0){
printf("create pro error\n");
return -1;
}
}
for (int i = 0; i < 3; i++){
pthread_join(ctid[i], NULL);
}
for (int i = 0; i < 3; i++){
pthread_join(ptid[i], NULL);
}
return 0;
}
通过两个线程组,使用同一个环形队列,进行同步与互斥处理,实现了生产者与消费者模型。