前言:在之前的进程间通信时我们就讲到过信号量,他的本质就是一个计数器,用来描述临界资源的一个计数器。我们当时使用电影院的例子来说明信号量。电影院的座位被我们称为临界资源,只有买到票才能有座位去看电影,申请信号量就是预定买票,申请成功才可以继续往下走下去。而我们看完电影就会释放临界资源,这些资源就可以被别人申请了。
目录
POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()
上述就是我信号量的基本操作,现在我们需要实现一个基于环形队列一个生产消费者模型,我们为什么不用上篇博客中的阻塞队列呢。因为阻塞队列我们将其看作一个整体只能进行首尾操作,不能访问队列中间内容。而环形队列我们使用的是vecotor数组进行模拟,可以使用下标进行访问中间内容。
基于环形队列的生产消费模型
环形队列采用数组模拟,用模运算来模拟环状特性
当hend与tail指向同一块位置时,数组可能为满也可能为空,其余的时候可以进行并发访问数组。
环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态。
但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程
ringqueue.hpp
#pragma once
#include<iostream>
#include<vector>
#include<semaphore.h>
#include<pthread.h>
using namespace std;
template<typename T>
class RingQueueu
{
private:
void P(sem_t& sem)
{
sem_wait(&sem);
}
void V(sem_t& sem)
{
sem_post(&sem);
}
void Lock(pthread_mutex_t& mutex)
{
pthread_mutex_lock(&mutex);
}
void Unlock(pthread_mutex_t& mutex)
{
pthread_mutex_unlock(&mutex);
}
public:
RingQueueu(int cap)
:_cap(cap),_ring_queue(cap)
{
sem_init(&_room_sem,0,_cap);
sem_init(&_data_sem,0,0);
pthread_mutex_init(&_productor_mutex,nullptr);
pthread_mutex_init(&_consumer_mutex,nullptr);
}
void Enqueue(const T& in)
{
Lock(_productor_mutex);
P(_room_sem);
_ring_queue[_productor_step++] = in;
_productor_step %= _cap;
V(_data_sem);
Unlock(_productor_mutex);
}
void Pop(T* out)
{
Lock(_consumer_mutex);
P(_data_sem);
*out = _ring_queue[_consumer_step++];
_consumer_step %= _cap;
V(_room_sem);
Unlock(_consumer_mutex);
}
~RingQueueu()
{
sem_destroy(&_room_sem);
sem_destroy(&_data_sem);
pthread_mutex_destroy(&_productor_mutex);
pthread_mutex_destroy(&_consumer_mutex);
}
private:
vector<T> _ring_queue;
int _cap;
int _productor_step = 0;
int _consumer_step = 0;
sem_t _room_sem;
sem_t _data_sem;
//加锁,维护多生产多消费
pthread_mutex_t _productor_mutex;
pthread_mutex_t _consumer_mutex;
};
单生产单消费时可以不用加锁,而多生产多消费时就需要加锁防止同时访问临界资源。
而加锁解锁应该放在申请信号量的后面进行才是比较好的,为什么呢?因为申请信号量是预定机制是原子的,不会出现线程安全问题, 这样可以先预定再等锁,锁来了之后直接运行后面代码,可以提高效率。(等也是等,不如再等的时候申请信号量)
所以这样写更好:
void Enqueue(const T &in)
{
P(_room_sem);
Lock(_productor_mutex);
_ring_queue[_productor_step++] = in;
_productor_step %= _cap;
Unlock(_productor_mutex);
V(_data_sem);
}
void Pop(T *out)
{
P(_data_sem);
Lock(_consumer_mutex);
*out = _ring_queue[_consumer_step++];
_consumer_step %= _cap;
Unlock(_consumer_mutex);
V(_room_sem);
}
main.cc
#include "RingQueue.hpp"
#include "Thread.hpp"
#include "Task.hpp"
#include <string>
#include <vector>
#include <unistd.h>
#include <ctime>
int a = 10;
using namespace ThreadModule;
using namespace std;
using ringqueue_t = RingQueueu<Task>;
void PrintHello()
{
cout << "hello" << endl;
}
void Consumer(ringqueue_t &rq)
{
while (true)
{
Task t;
rq.Pop(&t);
t();
}
}
void Productor(ringqueue_t &rq)
{
while (true)
{
sleep(1);
rq.Enqueue(Download);
}
}
void InitComm(std::vector<Thread<ringqueue_t>> *threads, int num, ringqueue_t &rq, func_t<ringqueue_t> func)
{
for (int i = 0; i < num; i++)
{
std::string name = "thread-" + std::to_string(i + 1);
threads->emplace_back(func, rq, name);
//(*threads)[threads->size()-1].Start();
//threads->back().Start();
}
}
void InitConsumer(std::vector<Thread<ringqueue_t>> *threads, int num, ringqueue_t &rq)
{
InitComm(threads, num, rq, Consumer);
}
void InitProductor(std::vector<Thread<ringqueue_t>> *threads, int num, ringqueue_t &rq)
{
InitComm(threads, num, rq, Productor);
}
void WaitAllThread(std::vector<Thread<ringqueue_t>> threads)
{
for(auto thread : threads)
{
thread.Join();
}
}
void StartAll(vector<Thread<ringqueue_t>>& threads)
{
for(auto& thread : threads)
{
thread.Start();
}
}
int main()
{
ringqueue_t *bq = new ringqueue_t(10);
std::vector<Thread<ringqueue_t>> threads;
InitConsumer(&threads, 1, *bq);
InitProductor(&threads, 1, *bq);
StartAll(threads);
WaitAllThread(threads);
return 0;
}
这里使用的也不是原生线程库,而是我们自己封装的thread。上述代码都是完整无误的,但是这里