目录
POSIX信号量
1. 为什么需要信号量?
2. 信号量的概念
3. 信号量函数
基于环形队列的生产消费模型
1.空间资源(SpaceSem)和数据资源(DataSem)
2.生产者和消费者申请和释放资源
3.消费者和生产者正常进行追逐游戏,必须满足的三个条件:
4.信号量保护环形队列的方法
5.代码实现
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
举个例子来加深理解:我们平时去看电影院看电影,电影院就相当于是临界资源,我们每个人都可以申请去电影院这个临界资源看电影,但是电影院有很多座位,这意味着不止电影院同一时刻不止允许一个人进行看电影,每个买了票的人都可以进行去电影院看电影。这样一来,电影院这块临界资源就可以在同一时刻让许多人去看电影!这样就提高了电影院这个临界资源的使用效率!
注意:我们申请到临界资源是我们买了票(申请到临界资源)就有访问的权限,而不是一定要去电影院坐着对应的位置才算拥有这块临界资源的访问权限。(我们买到票了位置就得给我们留着,但我们因为各种原因不一定会去电影院坐着对应的位置)
信号量(信号灯)本质是一个计数器,是描述临界资源中资源数目的计数器,信号量能够更细粒度的对临界资源进行管理。
每个执行流在进入临界区之前都应该先申请信号量,申请成功就有了操作特定的临界资源的权限,当操作完毕后就应该释放信号量。
信号量的PV操作:
- P操作:我们将申请信号量称为P操作,申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该 -1,因此P操作的本质就是让计数器 -1。
- V操作:我们将释放信号量称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该 +1,因此V操作的本质就是让计数器 +1。
注意:PV操作必须是原子操作
- 多个执行流为了访问临界资源会竞争式的申请信号量,因此信号量是会被多个执行流同时访问的,也就是说信号量本质也是临界资源。
- 但信号量本质就是用于保护临界资源的,我们不可能再用信号量去保护信号量,所以信号量的PV操作必须是原子操作。
- 内存当中变量的++、--操作并不是原子操作,因此信号量不可能只是简单的对一个全局变量进行++、--操作。
申请信号量失败被挂起等待
当执行流在申请信号量时,可能此时信号量的值为0,也就是说信号量描述的临界资源已经全部被申请了,此时该执行流就应该在该信号量的等待队列当中进行等待,直到有信号量被释放时再被唤醒。
注意: 信号量的本质是计数器,但不意味着只有计数器,信号量还包括一个等待队列。
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数说明:
- sem:需要初始化的信号量。
- pshared:0表示线程间共享,非零表示进程间共享
- value:信号量初始值
返回值:
- 初始化信号量成功返回0,失败返回-1。
int sem_destroy(sem_t *sem);
参数说明:
- sem:需要销毁的信号量。
返回值说明:
- 销毁信号量成功返回0,失败返回-1。
int sem_wait(sem_t *sem); //P()
功能:等待信号量,会将信号量的值减1
参数说明:
- sem:需要等待的信号量。
返回值说明:
- 等待信号量成功返回0,信号量的值-1。
- 等待信号量失败返回-1,信号量的值保持不变。
int sem_post(sem_t *sem);//V()
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值+1。
参数说明:
- sem:需要发布的信号量。
返回值说明:
- 发布信号量成功返回0,信号量的值+1。
- 发布信号量失败返回-1,信号量的值保持不变。
上一节生产者-消费者的例子是基于阻塞queue的,其空间可以动态分配,现在基于固定大小的环形队列重写这个程序(POSIX信号量):
对于生产者来说,生产者每次生产数据前都需要先申请SpaceSem:
如果SpaceSem的值不为0,则信号量申请成功,此时生产者可以进行生产操作。
如果SpaceSem的值为0,则信号量申请失败,此时生产者需要在SpaceSem的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒。
当生产者生产完数据后,应该释放data_sem:
当生产者生产完数据后,意味着环形队列当中多了一个data位置,因此我们应该对data_sem进行V操作。
对于消费者来说,消费者每次消费数据前都需要先申请data_sem:
如果data_sem的值不为0,则信号量申请成功,此时消费者可以进行消费操作。
如果data_sem的值为0,则信号量申请失败,此时消费者需要在data_sem的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒。
当消费者消费完数据后,应该释放SpaceSem:
当消费者消费完数据后,意味着环形队列当中多了一个Space位置,因此我们应该对SpaceSem进行V操作。
在基于环形队列的生产者和消费者模型当中,生产者和消费者必须遵守如三个规则。
1.指向同一个位置,只能一个人进行访问
生产者和消费者在访问环形队列时:
2.生产者不能将消费者套成一个圈。
3.消费者不能超过生产者
在space_sem和data_sem两个信号量的保护后,该环形队列中不可能会出现数据不一致的问题。
因为只有当生产者和消费者指向同一个位置并访问时,才会导致数据不一致的问题,而此时生产者和消费者在对环形队列进行写入或读取数据时,只有两种情况会指向同一个位置:
但是在这两种情况下,生产者和消费者不会同时对环形队列进行访问:
也就是说,当环形队列为空和满时,我们已经通过信号量保证了生产者和消费者的串行化过程。而除了这两种情况之外,生产者和消费者指向的都不是同一个位置,因此该环形队列当中不可能会出现数据不一致的问题。并且大部分情况下生产者和消费者指向并不是同一个位置,因此大部分情况下该环形队列可以让生产者和消费者并发的执行。
其中的RingQueue就是生产者消费者模型当中的交易场所,我们可以用C++STL库当中的vector进行实现。
RingQueue.hpp
#pragma once
#include
#include
#include
#include
using namespace std;
const static int defaultcap = 5;
template
class RingQueue
{
public:
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:
RingQueue(int cap = defaultcap)
:ringqueue_(cap),cap_(cap),p_step_(0),c_step_(0)
{
sem_init(&p_space_sem,0,cap);//p_space_sem初始值设置为环形队列的容量
sem_init(&c_data_sem,0,0);//c_data_sem初始值设置为0
pthread_mutex_init(&p_mutex_,nullptr);
pthread_mutex_init(&p_mutex_,nullptr);
}
//生产,向环形队列插入数据
void Push(const T& in)
{
P(p_space_sem);
Lock(p_mutex_);//为什么P在Lock外面?
ringqueue_[p_step_] = in;
// 位置后移,维持环形特性
p_step_++;
p_step_%=cap_;
UnLock(p_mutex_);
V(c_data_sem);
}
//消费,从环形队列获取数据
void Pop(T* out)
{
P(c_data_sem);
Lock(c_mutex_);
*out = ringqueue_[c_step_];
//位置后移,维持环形特性
c_step_++;
c_step_%=cap_;
UnLock(c_mutex_);
V(p_space_sem);
}
~RingQueue()
{
sem_destroy(&p_space_sem);
sem_destroy(&c_data_sem);
pthread_mutex_destroy(&p_mutex_);
pthread_mutex_destroy(&c_mutex_);
}
private:
vector ringqueue_;//环形队列
int cap_;//环形队列的容量上限
int p_step_;//生产者下标
int c_step_;//消费者下标
sem_t p_space_sem;//生产者关注的空间资源
sem_t c_data_sem;//消费者关注的数据资源
pthread_mutex_t p_mutex_;
pthread_mutex_t c_mutex_;
};
代码说明:
Task.hpp
#pragma once
#include
#include
using namespace std;
string opers = "+-*/%";
enum{
DivZero = 1,
ModZero,
Unknown
};
class Task
{
public:
Task()
{
}
Task(int data1,int data2,char oper)
:data1_(data1),data2_(data2),oper_(oper),result_(0),exitcode_(0)
{
}
void run()
{
switch (oper_)
{
case '+':
result_ = data1_ + data2_;
break;
case '-':
result_ = data1_ - data2_;
break;
case '*':
result_ = data1_ * data2_;
break;
case '/':
{
if(data2_ == 0) exitcode_ = DivZero;
else result_ = data1_ / data2_;
}
break;
case '%':
{
if(data2_ == 0) exitcode_ = ModZero ;
else result_ = data1_ % data2_;
}
break;
default:
exitcode_ = Unknown;
break;
}
}
void operator()()
{
run();
}
string GetResult()
{
string r = to_string(data1_);
r += oper_;
r += to_string(data2_);
r += "=";
r += to_string(result_);
r += "[code: ";
r += to_string(exitcode_);
r += "]";
return r;
}
string GetTask()
{
string r = to_string(data1_);
r += oper_;
r += to_string(data2_);
r += "=?";
return r;
}
~Task()
{}
private:
int data1_;
int data2_;
char oper_;
int result_;
int exitcode_;
};
为了方便理解,我们这里先实现单生产者、单消费者的生产者消费者模型。于是在主函数我们就只需要创建一个生产者线程和一个消费者线程,生产者线程不断生产数据放入环形队列,消费者线程不断从环形队列里取出数据进行消费。
main.cpp
#include
#include
#include
#include
#include"RingQueue.hpp"
#include"Task.hpp"
using namespace std;
struct ThreadData
{
RingQueue *rq;
string threadname;
};
void* Productor(void* args)
{
ThreadData* td = static_cast(args);
RingQueue *rq = td->rq;
string name = td->threadname;
int len = opers.size();
while (true)
{
//1.获取数据
int data1 = rand()%10+1;
usleep(10);
char oper = opers[rand()%len];
int data2 = rand()%5;
Task t(data1,data2,oper);
//2.生产数据
rq->Push(t);
cout << "Productor task done, task is" << t.GetTask() << "who:" << name <(args);
RingQueue *rq = td->rq;
string name = td->threadname;
while (true)
{
sleep(1);
//1.消费数据
Task t;
rq->Pop(&t);
//2.处理数据
t();
cout << "Consumer get task, task is :" << t.GetTask() << " who: " << name << "result:" << t.GetResult() << endl;
}
return nullptr;
}
int main()
{
srand(time(nullptr)^getpid());
RingQueue* rq = new RingQueue(10);
pthread_t p[3],c[5];
for(int i = 0;i < 1;++i)
{
ThreadData* td = new ThreadData();
td->rq = rq;
td->threadname = "Productor-" + to_string(i);
pthread_create(p+i,nullptr,Productor,td);
}
for(int i = 0;i < 1;++i)
{
ThreadData* td = new ThreadData();
td->rq = rq;
td->threadname = "Consumer-" + to_string(i);
pthread_create(c+i,nullptr,Consumer,td);
}
for (int i = 0; i < 1; i++)
{
pthread_join(p[i],nullptr);
}
for (int i = 0; i < 1; i++)
{
pthread_join(c[i],nullptr);
}
return 0;
}
代码说明:
生产者消费者步调一致
代码中我们让生产者每个一秒生产一次,消费者每隔一秒消费一次,因此运行代码后我们可以看到生产者和消费者的执行步调是一致的。
生产者生产的快,消费者消费的慢
我们可以让生产者不停的进行生产,而消费者每隔一秒进行消费。
void* Productor(void* args)
{
ThreadData* td = static_cast(args);
RingQueue *rq = td->rq;
string name = td->threadname;
int len = opers.size();
while (true)
{
//1.获取数据
int data1 = rand()%10+1;
usleep(10);
char oper = opers[rand()%len];
int data2 = rand()%5;
Task t(data1,data2,oper);
//2.生产数据
rq->Push(t);
cout << "Productor task done, task is" << t.GetTask() << "who:" << name <(args);
RingQueue *rq = td->rq;
string name = td->threadname;
while (true)
{
sleep(1);
//1.消费数据
Task t;
rq->Pop(&t);
//2.处理数据
t();
cout << "Consumer get task, task is :" << t.GetTask() << " who: " << name << "result:" << t.GetResult() << endl;
}
return nullptr;
}
运行结果:
此时由于生产者生产的很快,运行代码后一瞬间生产者就将环形队列打满了,此时生产者想要再进行生产,但空间资源已经为0了,于是生产者只能在p_space_sem的等待队列下进行阻塞等待,直到由消费者消费完一个数据后对p_space_sem进行了V操作,生产者才会被唤醒进而继续进行生产。
但由于生产者的生产速度很快,生产者生产完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了。
生产者生产的慢,消费者消费的快
当然我们也可以让生产者每隔一秒进行生产,而消费者不停的进行消费。
void* Productor(void* args)
{
ThreadData* td = static_cast(args);
RingQueue *rq = td->rq;
string name = td->threadname;
int len = opers.size();
while (true)
{
//1.获取数据
int data1 = rand()%10+1;
usleep(10);
char oper = opers[rand()%len];
int data2 = rand()%5;
Task t(data1,data2,oper);
//2.生产数据
rq->Push(t);
cout << "Productor task done, task is" << t.GetTask() << "who:" << name <(args);
RingQueue *rq = td->rq;
string name = td->threadname;
while (true)
{
//1.消费数据
Task t;
rq->Pop(&t);
//2.处理数据
t();
cout << "Consumer get task, task is :" << t.GetTask() << " who: " << name << "result:" << t.GetResult() << endl;
}
return nullptr;
}
运行结果:
虽然消费者消费的很快,但一开始环形队列当中的数据资源为0,因此消费者只能在c_data_sem的等待队列下进行阻塞等待,直到生产者生产完一个数据后对c_data_sem进行了V操作,消费者才会被唤醒进而进行消费。
但由于消费者的消费速度很快,消费者消费完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了。