信号量(信号灯)本质是一个计数器,是描述临界资源中资源数目的计数器,信号量能够更细粒度的对临界资源进行管理。
每个执行流在进入临界区之前都应该先申请信号量,申请成功就有了操作特点的临界资源的权限,当操作完毕后就应该释放信号量。
信号量的PV操作:
注意:PV操作必须是原子性的,因为在多线程情况下会存在多个执行流去竞争一个信号量的情况,所以信号量也是一个临界资源,而信号量的本质是为了保护临界资源的,我们就不可能让信号量去保护信号量了,所以信号量的PV操作必须是原子操作。
当执行流在申请信号量时,可能此时信号量的值为0,也就是说信号量描述的临界资源已经全部被申请了,此时该执行流就应该在该信号量的等待队列当中进行等待,直到有信号量被释放时再被唤醒。也就是说,信号量的本质是计数器,但不意味着只有计数器,信号量还包括一个等待队列。
初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数说明:
sem
:需要初始化的信号量。pshared
:传入0值表示线程间共享,传入非零值表示进程间共享。value
:信号量的初始值(计数器的初始值)。返回值说明:
注意: POSIX信号量和System V信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的,但POSIX信号量可以用于线程间同步。
销毁信号量
销毁信号量的函数叫做sem_destroy,该函数的函数原型如下:
int sem_destroy(sem_t *sem);
参数说明:
返回值说明:
等待信号量(申请信号量)
等待信号量的函数叫做sem_wait,该函数的函数原型如下:
int sem_wait(sem_t *sem);
参数说明:
返回值说明:
发布信号量(释放信号量)
发布信号量的函数叫做sem_post,该函数的函数原型如下:
int sem_post(sem_t *sem);
参数说明:
返回值说明:
在生产者与消费者模型中,生产者需要生产资源,所以需要的是空间资源,消费者需要消费资源,所以需要的是数据资源。
对于生产者和消费者来说,它们关注的资源是不同的:
现在我们用信号量来描述环形队列当中的空间资源(space_sem)和数据资源(data_sem),在我们初始信号量时给它们设置的初始值是不同的:
环形队列的生产者和消费者模型当中,生产者和消费者必须遵守如下两个规则:
1.生产者和消费者不能对同一个位置进行访问。
2.无论是生产者还是消费者,都不应该将对方套一个圈以上。
对于生产者来说,每一次操作生产者需要申请空间资源,释放数据资源。
生产者每次生产数据前都需要先申请space_sem:
当生产者生产完数据后,应该释放data_sem:
消费者每一次操作需要申请数据资源,释放空间资源。
对于消费者来说,消费者每次消费数据前都需要先申请data_sem:
当消费者消费完数据后,应该释放space_sem:
首先,我们可以对信号量函数进行包装,方便调用:
#ifndef _SEM_HPP_
#define _SEM_HPP_
#include
#include
class Sem
{
public:
Sem(int value)
{
sem_init(&sem_, 0, value);
}
void p()
{
sem_wait(&sem_);
}
void v()
{
sem_post(&sem_);
}
~Sem()
{
sem_destroy(&sem_);
}
private:
sem_t sem_;
};
#endif
其次,我们得实现一个环形队列,我们可以使用STL库中的vector容器来实现:
#pragma once
#ifndef _Ring_QUEUE_HPP_
#define _Ring_QUEUE_HPP_
#include
#include
#include
#include
#include "Sem.hpp"
#define NUM 5
template <class T>
class RingQueue
{
public:
// 构造函数
RingQueue(int num = NUM)
: _ring_queue_(NUM)
, num_(NUM)
, c_step_(0)
, p_step_(0)
, space_sem_(NUM)
, data_sem_(0)
{
}
// 析构函数
~RingQueue()
{
}
// 生产者生产资源
void push(const T &in)
{
// 申请信号量
space_sem_.p();
_ring_queue_[p_step_++] = in;
p_step_ %= num_;
data_sem_.v();
}
// 消费者消费资源
void pop(T &out)
{
// 申请信号量
data_sem_.p();
out = _ring_queue_[c_step_++];
c_step_ %= num_;
space_sem_.v();
}
private:
std::vector<T> _ring_queue_;
int num_;
int c_step_; // 生产者下标
int p_step_; // 消费者下标
Sem space_sem_;
Sem data_sem_;
};
#endif
代码说明:
我们这里实现单生产者、单消费者的生产者消费者模型。于是在主函数我们就只需要创建一个生产者线程和一个消费者线程,生产者线程不断生产数据放入环形队列,消费者线程不断从环形队列里取出数据进行消费。
#include "RingQueue.hpp"
#include
#include
void *consumer(void *args)
{
RingQueue<int> *rq = (RingQueue<int> *)args;
while (true)
{
int x;
rq->pop(x);
std::cout << "消费:" << x << std::endl;
}
}
void *productor(void *args)
{
RingQueue<int> *rq = (RingQueue<int> *)args;
while (true)
{
int x = rand() % 100 + 1;
std::cout << "生产:" << x << std::endl;
rq->push(x);
}
}
int main()
{
srand((uint64_t)time(nullptr) ^ getpid());
RingQueue<int> *rq = new RingQueue<int>();
pthread_t c, p;
pthread_create(&c, nullptr, consumer, (void *)rq);
pthread_create(&p, nullptr, productor, (void *)rq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
生产者与消费者步调一致
由于代码中生产者是每隔一秒生产一个数据,而消费者是每隔一秒消费一个数据,因此运行代码后我们可以看到生产者和消费者的执行步调是一致的:
生产者生产的快,消费者消费的慢
我们可以让生产者不停的进行生产,而消费者每隔一秒进行消费,此时由于生产者生产的很快,运行代码后一瞬间生产者就将环形队列打满了,此时生产者想要再进行生产,但空间资源已经为0了,于是生产者只能在space_sem的等待队列下进行阻塞等待,直到由消费者消费完一个数据后对space_sem进行了V操作,生产者才会被唤醒进而继续进行生产。
但由于生产者的生产速度很快,生产者生产完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了。
生产者生产的慢,消费者消费的快
我们也可以让生产者每隔一秒进行生产,而消费者不停的进行消费,虽然消费者消费的很快,但一开始环形队列当中的数据资源为0,因此消费者只能在data_sem的等待队列下进行阻塞等待,直到生产者生产完一个数据后对data_sem进行了V操作,消费者才会被唤醒进而进行消费。
但由于消费者的消费速度很快,消费者消费完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了。
我们也可以在多线程情况下进行生产和消费,但是此时就需要引入互斥锁,因为信号量本身也属于临界资源,多线程情况下,我们无法保证单个执行流对其进行访问,所以我们需要对生产者和消费者分别引入一把互斥锁:
#pragma once
#ifndef _Ring_QUEUE_HPP_
#define _Ring_QUEUE_HPP_
#include
#include
#include
#include
#include "Sem.hpp"
#define NUM 5
template <class T>
class RingQueue
{
public:
// 构造函数
RingQueue(int num = NUM)
: _ring_queue_(NUM), num_(NUM), c_step_(0), p_step_(0), space_sem_(NUM), data_sem_(0)
{
pthread_mutex_init(&p_lock, nullptr);
pthread_mutex_init(&c_lock, nullptr);
}
// 析构函数
~RingQueue()
{
pthread_mutex_destroy(&p_lock);
pthread_mutex_destroy(&c_lock);
}
// 生产者生产资源
void push(const T &in)
{
// 申请信号量
space_sem_.p();
// 申请锁
pthread_mutex_lock(&p_lock);
_ring_queue_[p_step_++] = in;
p_step_ %= num_;
// 释放锁
pthread_mutex_unlock(&p_lock);
data_sem_.v();
}
// 消费者消费资源
void pop(T &out)
{
// 申请信号量
data_sem_.p();
// 申请锁
pthread_mutex_lock(&c_lock);
out = _ring_queue_[c_step_++];
c_step_ %= num_;
// 释放锁
pthread_mutex_unlock(&c_lock);
space_sem_.v();
}
private:
std::vector<T> _ring_queue_;
int num_;
int c_step_; // 生产者下标
int p_step_; // 消费者下标
Sem space_sem_;
Sem data_sem_;
pthread_mutex_t p_lock;
pthread_mutex_t c_lock;
};
#endif
主函数如下:
#include "RingQueue.hpp"
#include
#include
void *consumer(void *args)
{
RingQueue<int> *rq = (RingQueue<int> *)args;
while (true)
{
sleep(1);
int x;
rq->pop(x);
std::cout << "消费:" << x << "[" << pthread_self() << "]" << std::endl;
}
}
void *productor(void *args)
{
RingQueue<int> *rq = (RingQueue<int> *)args;
while (true)
{
int x = rand() % 100 + 1;
std::cout << "生产:" << x << "[" << pthread_self() << "]" << std::endl;
rq->push(x);
}
}
int main()
{
srand((uint64_t)time(nullptr) ^ getpid());
RingQueue<int> *rq = new RingQueue<int>();
pthread_t c[3], p[2];
pthread_create(c, nullptr, consumer, (void *)rq);
pthread_create(c + 1, nullptr, consumer, (void *)rq);
pthread_create(c + 2, nullptr, consumer, (void *)rq);
pthread_create(p, nullptr, productor, (void *)rq);
pthread_create(p + 1, nullptr, productor, (void *)rq);
for (int i = 0; i < 3; i++)
pthread_join(c[i], nullptr);
for (int i = 0; i < 2; i++)
pthread_join(p[i], nullptr);
return 0;
}
在space_sem和data_sem两个信号量的保护后,该环形队列中不可能会出现数据不一致的问题。
在环形队列中,只有生产者跟消费者访问同一位置才会出现数据不一致的问题,而生产者与消费者只有在两种情况下才会指向同一位置:
但是在这两种情况下,生产者和消费者不会同时对环形队列进行访问:
也就是说,当环形队列为空和满时,我们已经通过信号量保证了生产者和消费者的串行化过程。而除了这两种情况之外,生产者和消费者指向的都不是同一个位置,因此该环形队列当中不可能会出现数据不一致的问题。并且大部分情况下生产者和消费者指向并不是同一个位置,因此大部分情况下该环形队列可以让生产者和消费者并发的执行。