目录
POSIX信号量
信号量的原理
- 信号量本质是一把计数器
- 申请信号量本质是预定机制
- P、V 操作是原子的
- 我们将可能会被多个执行流同时访问的资源叫做临界资源,临界资源需要进行保护否则会出现数据不一致等问题
- 当我们仅用一个互斥锁对临界资源进行保护时,相当于我们将这块临界资源看作一个整体,同一时刻只允许一个执行流对这块临界资源进行访问。、
- 我们不将临界资源看作一个整体,我们可以将这块临界资源再分割为多个区域,当多个执行流需要访问临界资源时,如果这些执行流访问的是临界资源的不同区域,那么我们可以让这些执行流同时访问临界资源的不同区域,此时不会出现数据不一致等问题。
- 为了防止我们分配多的临界资源的区域,比如下图就 5 个区域,我们却分配出 8 个区域。我们需要用到信号量
- 每个执行流在进入临界区之前都应该先申请信号量,申请成功就有了操作特点的临界资源的权限
- 访问临界资源指定的一个位置
- 当操作完毕后就应该释放信号量
信号量的PV操作:
- P操作:我们将申请信号量称为P操作,申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减一,因此P操作的本质就是让计数器减一。
- V操作:我们将释放信号量称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该加一,因此V操作的本质就是让计数器加一。
为什么 PV操作必须是原子操作
- 多个执行流为了访问临界资源会竞争式的申请信号量,因此信号量是会被多个执行流同时访问的,也就是说信号量本质也是临界资源。
- 但信号量本质就是用于保护临界资源的,我们不可能再用信号量去保护信号量,所以信号量的PV操作必须是原子操作。
注意: 内存当中变量的++、--操作并不是原子操作,因此信号量不可能只是简单的对一个全局变量进行++、--操作。
信号量函数
初始化信号量
初始化信号量的函数叫做 sem_init ,该函数的函数原型如下:
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem
:指向要初始化的信号量对象的指针。pshared
:决定信号量的共享范围。- 如果
pshared
为 0,表示信号量在同一进程内的多个线程间共享。 - 如果
pshared
为非零值,表示信号量在不同进程间共享,这要求信号量位于共享内存区域。
- 如果
value
:信号量的初始值
返回值:
- 如果成功初始化信号量,返回 0。
- 如果出现错误,返回 -1,并设置
errno
以指示错误类型。可能的错误包括:EINVAL
:参数无效。ENOSYS
:系统不支持进程间共享信号量(当pshared
非零时出现此错误可能表明系统不支持该功能)。ENOMEM
:内存不足,无法初始化信号量。
销毁信号量
销毁信号量的函数叫做 sem_destroy,该函数的函数原型如下:
int sem_destroy(sem_t *sem);
参数:
sem
:指向要销毁的信号量对象的指针。
返回值:
- 如果成功销毁信号量,返回 0。
- 如果出现错误,返回 -1,并设置
errno
以指示错误类型。可能的错误情况相对较少,常见的错误是在尝试销毁一个正在被其他线程或进程等待的信号量时会出错。
等待信号量(申请信号量)
等待信号量的函数叫做 sem_wait,该函数的函数原型如下:
int sem_wait(sem_t *sem);
参数:
sem
:指向要等待的信号量对象的指针。
返回值:
- 如果成功等待到信号量(即信号量的值大于 0),函数返回 0。
- 如果出现错误,返回 -1,并设置
errno
以指示错误类型。可能的错误包括:EINTR
:等待被信号中断。EINVAL
:信号量参数无效。
发布信号量(释放信号量)
发布信号量的函数叫做 sem_post,该函数的函数原型如下:
int sem_post(sem_t *sem);
参数:
sem
:指向要操作的信号量对象的指针。
返回值:
- 如果成功增加信号量的值并唤醒等待线程(如果有),返回 0。
- 如果出现错误,返回 -1,并设置
errno
以指示错误类型。可能的错误包括:EINVAL
:信号量参数无效。
基于环形队列的生产消费模型
两个规则
在基于环形队列的生产者和消费者模型当中,生产者和消费者必须遵守如下两个规则
- 生产者(Product)或消费者(Consumer)不能把对方围成一个圈 !
- 生产者(Product)和消费者(Consumer)不能对同一个位置进行访问
为什么生产者(Product)或消费者(Consumer)不能把对方围成一个圈 !
- 生产者(Product)从消费者(Consumer)的位置开始一直按顺时针方向进行生产,如果生产者(Product)生产的速度比消费者(Consumer)消费的速度快,那么当生产者(Product)绕着消费者(Consumer)生产了一圈数据后再次遇到消费者(Consumer),此时生产者(Product)就不应该再继续生产了,因为再生产就会覆盖还未被消费者(Consumer)消费的数据。
- 消费者从生产者的位置开始一直按顺时针方向进行消费,如果消费者消费的速度比生产者生产的速度快,那么当消费者绕着生产者消费了一圈数据后再次遇到生产者,此时消费者就不应该再继续消费了,因为再消费就会消费到缓冲区中保存的废弃数据。
为什么生产者(Product)和消费者(Consumer)不能对同一个位置进行访问?
- 如果生产者和消费者访问的是环形队列当中的同一个位置,那么此时生产者和消费者就相当于同时对这一块临界资源进行了访问(会出现数据不一致),这当然是不允许的。
- 而如果生产者和消费者访问的是环形队列当中的不同位置,那么此时生产者和消费者是可以同时进行生产和消费的,此时不会出现数据不一致等问题。
“资源”的认识
对于生产者和消费者来说,它们关注的资源是不同的:
- 生产者(Product)关注的是环形队列当中是否有空间(space),只要有空间生产者(Product)就可以进行生产。
- 消费者关注的是环形队列当中是否有数据(data),只要有数据消费者就可以进行消费。
空间信号量(space_sem)和数据信号量(data_sem ) 信号量的初始值设置:
- space_sem的初始值我们应该设置为环形队列的容量,因为刚开始时环形队列当中全是空间。
- data_sem的初始值我们应该设置为0,因为刚开始时环形队列当中没有数据。
生产者和消费者申请和释放资源
生产者:1、申请空间资源(space) 2、释放数据资源(data)
- 先申请空间信号量(space_sem)
- 如果 space_sem 的值不为0,则信号量申请成功,此时生产者可以进行生产操作。
- 如果 space_sem 的值为0,则信号量申请失败,此时生产者需要在 space_sem 的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒。
- 当生产者生产完数据后,应该释放数据信号量(data_sem )
- 虽然生产者在进行生产前是对 space_sem 进行的P操作(申请信号量),但是当生产者生产完数据,应该对 data_sem 进行V操作而不是 space_sem
- 生产者在生产数据前申请到的是空间(space)位置,当生产者生产完数据后,该位置当中存储的是生产者生产的数据,在该数据被消费者消费之前,该位置不再是空间(space)位置,而应该是数据(data)位置
- 当生产者生产完数据后,意味着环形队列当中多了一个数据(data)位置,因此我们应该对 data_sem 进行V操作。
消费者:1、申请数据资源(data)2、释放空间资源(space)
- 先申请数据信号量(data_sem )
- 如果 data_sem 的值不为0,则信号量申请成功,此时消费者可以进行消费操作。
- 如果 data_sem 的值为0,则信号量申请失败,此时消费者需要在 data_sem 的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒。
- 当消费者消费完数据后,应该释放空间信号量(space_sem)
- 虽然消费者在进行消费前是对 data_sem 进行的P操作,但是当消费者消费完数据,应该对 space_sem 进行V操作而不是 data_sem
- 消费者在消费数据前申请到的是数据(data)位置,当消费者消费完数据后,该位置当中的数据已经被消费过了,再次被消费就没有意义了,为了让生产者后续可以在该位置生产新的数据,我们应该将该位置算作空间(space)位置,而不是数据(data)位置
- 当消费者消费完数据后,意味着环形队列当中多了一个空间(space)位置,因此我们应该对space_sem 进行V操作。
代码实现
单生产 单消费
RingQueue就是生产者消费者模型当中的交易场所,我们可以用C++STL库当中的vector进行封装实现
#include <iostream>
#include <pthread.h>
#include <vector>
#include <semaphore.h>
#include <unistd.h>
#define N 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 = N)
:_ringqueue(size),_size(size),_p_step(0),_c_step(0)
{
sem_init(&_space_sem, 0, size);
sem_init(&_data_sem, 0, 0);
}
~RingQueue()
{
sem_destroy(&_space_sem);
sem_destroy(&_data_sem);
}
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);
}
private:
std::vector<int> _ringqueue;
int _size;
int _p_step; // 生产者的位置
int _c_step; // 消费者的位置
sem_t _space_sem; // 空间信号量
sem_t _data_sem; // 数据信号量
};
主函数 main.cpp
#include "RingQueue.hpp"
// 生产者
void *Productor(void* args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
int cnt = 100;
while (true)
{
rq->push(cnt);
std::cout << "Product done, data is :" << cnt << std::endl;
cnt--;
}
}
// 消费者
void *Consumer(void* args)
{
RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
while (true)
{
int data = 0;
rq->pop(&data);
std::cout << "consumer done, data is :" << data << std::endl;
sleep(1);
}
}
int main()
{
pthread_t p,c; // p生产者的线程ID, c消费者的线程ID
RingQueue<int> *rq = new RingQueue<int>();
// 线程创建
pthread_create(&p,nullptr,Productor,rq);
pthread_create(&c,nullptr,Consumer,rq);
// 线程等待
pthread_join(p,nullptr);
pthread_join(c,nullptr);
return 0;
}
生产者消费者步调一致
生产者是每隔 1 秒生产一个数据,而消费者是每隔 1 秒消费一个数据
- 因此运行代码后我们可以看到生产者和消费者的执行步调是一致的
生产者生产的快,消费者消费的慢
我们可以让生产者不停的进行生产,而消费者每隔 1 秒进行消费
- 此时由于生产者生产的很快,运行代码后一瞬间生产者就将环形队列打满了,此时生产者想要再进行生产,但 空间资源(space)已经为0了
- 于是生产者只能在空间信号量(space_sem)的等待队列下进行阻塞等待,直到由消费者消费完一个数据后对空间信号量(space_sem)进行了V操作,生产者才会被唤醒进而继续进行生产。
- 但由于生产者的生产速度很快,生产者生产完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了。
生产者生产的慢,消费者消费的快
我们也可以让生产者每隔 1 秒进行生产,而消费者不停的进行消费
- 虽然消费者消费的很快,但一开始环形队列当中的数据资源(data)为0,因此消费者只能在数据信号量(data_sem)的等待队列下进行阻塞等待
- 直到生产者生产完一个数据后对数据信号量(data_sem)进行了V操作,消费者才会被唤醒进而进行消费。
- 但由于消费者的消费速度很快,消费者消费完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了。
多生产 多消费
#include <iostream>
#include <pthread.h>
#include <vector>
#include <semaphore.h>
#include <unistd.h>
#include "LockGuard.hpp"
#define N 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 = N)
:_ringqueue(size),_size(size),_p_step(0),_c_step(0)
{
sem_init(&_space_sem, 0, size);
sem_init(&_data_sem, 0, 0);
}
~RingQueue()
{
sem_destroy(&_space_sem);
sem_destroy(&_data_sem);
}
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);
}
private:
std::vector<T> _ringqueue;
int _size;
int _p_step; // 生产者的位置
int _c_step; // 消费者的位置
sem_t _space_sem; // 空间信号量
sem_t _data_sem; // 数据信号量
pthread_mutex_t _p_mutex; // 生产者的锁
pthread_mutex_t _c_mutex; // 消费者的锁
};
主函数 main.cpp
#include "RingQueue.hpp"
#include "Task.hpp"
// 生产者
void *Productor(void* args)
{
RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
while (true)
{
int data1 = rand() % 10; // [1, 10] // 将来深刻理解生产消费,就要从这里入手,TODO
usleep(rand() % 123);
int data2 = rand() % 10; // [1, 10] // 将来深刻理解生产消费,就要从这里入手,TODO
usleep(rand() % 123);
char oper = opers[rand() % (opers.size())];
Task t(data1, data2, oper);
std::cout << "productor task: " << t.PrintTask() << std::endl;
rq->push(t);
}
}
// 消费者
void *Consumer(void* args)
{
RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
while (true)
{
Task data ;
rq->pop(&data);
data();
std::cout << "consumer done, data is : " << data.PrintResult() << std::endl;
sleep(1);
}
}
int main()
{
pthread_t p[3],c[2]; // p生产者的线程ID, c消费者的线程ID
RingQueue<Task> *rq = new RingQueue<Task>();
// 线程创建
pthread_create(&p[0], nullptr, Productor, rq);
pthread_create(&p[1], nullptr, Productor, rq);
pthread_create(&c[0], nullptr, Consumer, rq);
pthread_create(&c[1], nullptr, Consumer, rq);
pthread_create(&c[2], nullptr, Consumer, rq);
// 线程等待
pthread_join(p[0], nullptr);
pthread_join(p[1], nullptr);
pthread_join(c[0], nullptr);
pthread_join(c[1], nullptr);
pthread_join(c[2], nullptr);
return 0;
}
Task.hpp
#pragma once
#include <iostream>
#include <string>
#include <unistd.h>
const int defaultvalue = 0;
enum
{
ok = 0,
div_zero,
mod_zero,
unknow
};
const std::string opers = "+-*/%)(&";
class Task
{
public:
Task()
{
}
Task(int x, int y, char op)
: data_x(x), data_y(y), oper(op), result(defaultvalue), code(ok)
{
}
void Run()
{
switch (oper)
{
case '+':
result = data_x + data_y;
break;
case '-':
result = data_x - data_y;
break;
case '*':
result = data_x * data_y;
break;
case '/':
{
if (data_y == 0)
code = div_zero;
else
result = data_x / data_y;
}
break;
case '%':
{
if (data_y == 0)
code = mod_zero;
else
result = data_x % data_y;
}
break;
default:
code = unknow;
break;
}
}
void operator()()
{
Run();
// sleep(2);
}
std::string PrintTask()
{
std::string s;
s = std::to_string(data_x);
s += oper;
s += std::to_string(data_y);
s += "=?";
return s;
}
std::string PrintResult()
{
std::string s;
s = std::to_string(data_x);
s += oper;
s += std::to_string(data_y);
s += " = ";
s += std::to_string(result);
s += " [";
s += std::to_string(code);
s += "]";
return s;
}
~Task()
{
}
private:
int data_x;
int data_y;
char oper; // + - * / %
int result;
int code; // 结果码 0: 结果可信 !0: 结果不可信,1,2,3,4
};
LockGuard.hpp
#pragma once
#include <pthread.h>
// 不定义锁,默认认为外部会给我们传入锁对象
class Mutex
{
public:
Mutex(pthread_mutex_t *lock):_lock(lock)
{}
void Lock()
{
pthread_mutex_lock(_lock);
}
void Unlock()
{
pthread_mutex_unlock(_lock);
}
~Mutex()
{}
private:
pthread_mutex_t *_lock;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t *lock): _mutex(lock)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.Unlock();
}
private:
Mutex _mutex;
};
信号量保护环形队列的原理
在空间信号量(space_sem)和数据信号量(data_sem)两个信号量的保护后,该环形队列中不可能会出现数据不一致的问题。
因为只有当生产者和消费者指向同一个位置并访问时,才会导致数据不一致的问题,而此时生产者和消费者在对环形队列进行写入或读取数据时,只有两种情况会指向同一个位置:
- 环形队列为空时
- 环形队列为满时
但是在这两种情况下,生产者和消费者不会同时对环形队列进行访问:
- 当环形队列为空的时,消费者一定不能进行消费,因为此时数据资源(data)为0
- 当环形队列为满的时,生产者一定不能进行生产,因为此时空间资源(space)为0
总结:
- 当环形队列为空(满)时,我们已经通过信号量保证了生产者和消费者的串行化过程
- 而除了这两种情况之外,生产者和消费者指向的都不是同一个位置,因此该环形队列当中不可能会出现数据不一致的问题
- 并且大部分情况下生产者和消费者指向并不是同一个位置,因此大部分情况下该环形队列可以让生产者和消费者并发的执行