【线程同步与互斥】:POSIX 信号量,PV操作、基于环形队列的生产消费模型(vector+信号量+互斥量)
目录
1. POSIX 信号量
1. 信号量是什么?
- 信号灯,本质是一个计数器,用来表明临界资源中,资源的数量的多少。(如电影院售票时,剩余的票数,剩多少票就表示剩余多少座位)。
2. 所有进程,访问临界资源中的一小块,就必须先申请信号量!
3. 进程访问资源前,先申请信号量,本质是:对资源的预定机制!!!
4.申请到信号量后,资源就是你的了,等待进程随时访问,没人和你抢!
细节1:信号量本身就是共享资源!
- 申请--,原子性,P操作
- (信号量)sem++,原子性,V操作
细节2:信号量只有 1 或 0 两态的信号量,叫做二元信号量!
计数器就是通过 PV 操作来完成资源的预定机制。
POSIX信号量和System V信号量作用相同,都用于同步操作,以实现无冲突地访问共享资源。但POSIX信号量可以用于线程间同步。
总结:
1. 信号量本质是一个计数器,是对特定资源的预定机制!
2. 多线程使用资源,有两种场景:
- 将目标资源整体使用【可以使用 mutex + 二元信号量保护临界资源】
- 将目标资源按照不同的 “块”,分批使用【可以使用信号量保护临界资源】
3. 所有的线程,都得先看到 sem,计数器,sem--,sem++
4. 信号量本质也是临界资源。
5. PV 操作都是原子的【P--,V++】。
6. 信号量 P 原子,申请成功,继续运行,申请失败,申请的线程会被阻塞。
1.1 初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
- pshared:0 表示信号量线程间共享,非零表示信号量进程间共享。
- value:信号量初始值。
1.2 销毁信号量
int sem_destroy(sem_t *sem);
1.3 等待信号量【P操作】
- sem_wait 作用:等待信号量,会将信号量的值减 1。
- Proberen ,尝试、测试。
int sem_wait(sem_t *sem); //P()
1.4 发布信号量【V操作】
- sem_post 作用:发布信号量,表示资源使用完毕,可以归还资源了,将信号量值加 1。
- Verhogen,增加、提高。
int sem_post(sem_t *sem);//V()
进一步理解信号量:
- 信号量的另一个本质:信号量把对临界资源是否存在?就绪?等的条件,以原子性的形式,呈现在访问临界资源之前就判断了!
- 即:信号量让你在进入临界区之前,就已经确切地知道“有没有资源可用”,而不需要进去之后才发现没有,再退出来。
- 唤醒队列,固定大小,我们自己用自己的下标访问的。
- 如果资源可以拆分,可以考虑使用 sem。
- 如果资源是整体使用的,就可以使用 mutex。
2. 基于环形队列的生产消费模型
环形队列采用数组模拟,借助模运算实现环状特性。

环形结构起始状态与结束状态一致,难以直接判断队空或队满。可通过增设计数器或标记位来判定队列空满;也可预留一个空闲位置,以此区分队满状态。

1. 空的时候:head == tail
2. 满的时候:head == tail
3. 如何判断环形队列是否为空为满?
- 方案1:使用计数器 conut 记录资源使用情况。
- 方案2:空一格位置,head == tail 时为空,(tail + 1)%N == head 时为满。
约定1:空,生产者先运行。
约定2:满,消费者先运行。
约定3:生产者不能把消费者套一个圈以上(会覆盖未访问的资源)。
约定4:消费者,不能超过消费者(超前消费)。
思考:
1. 只要我们不访问同一个位置,我们就可以同时进行!
2. 什么时候,我们会在同一个位置呢?为空或者为满的时候!!如果不为空,不为满,就能同时进行!!
3. 为空:只能【互斥】生产者先【同步】运行。
4. 为满:只能【互斥】消费者先【同步】运行。
2.1 RingQueue.hpp:
要实现环形队列的生产消费模型,需要两个信号量分别控制生产者和消费者的操作。
此外,还需要两把互斥锁来分别管理:
- 生产者之间的互斥
- 消费者之间的互斥
这与之前使用queue实现的生产消费模型有所不同。queue模型只需要一把锁,因为它用于同步生产者和消费者之间的互斥访问。
两者的关键区别在于数据结构特性:
- Vector + 信号量:通过下标直接访问特定位置,生产者和消费者可以并行操作不同的内存区域,因此需要两把锁分别保护各自的操作位置。
- Queue + 条件变量:只能从队首或队尾操作,生产和消费都必须修改队列结构(包括头尾指针和内部节点),这些修改必须串行执行,因此一把锁就足以保护整个队列。
#pragma once
#include <iostream>
#include <vector>
#include "Mutex.hpp"
#include "Sem.hpp"
using namespace MutexModule;
using namespace SemModule;
const int gcap = 5; // 测试
// 使用模板插入不同类型的数据
template <typename T>
class RingQueue
{
public:
RingQueue(const int cap = gcap)
: _rq(cap), // 数组大小
_cap(cap),
_blank_sem(cap),
_data_sem(0),
_p_step(0),
_c_step(0)
{
}
// 生产者接口
void Equeue(const T &in)
{
// 先申请信号量,先抢夺临界资源,在抢夺访问临界资源
_blank_sem.P();// 等待信号量,生产者信号量--
{
LockGuard lockguard(_pmutex); // 构造申请锁,析构释放锁
_rq[_p_step] = in; // 插入数据
_p_step++; // 维护信号量
_p_step %= _cap; // 保持环形也行
}
_data_sem.V();// 发布信号量,消费者信号量++
}
// 消费者接口
void Pop(T *out)
{
_data_sem.P();// 申请信号量
{
LockGuard lockguard(_cmutex);
*out=_rq[_c_step];
_c_step++;
_c_step%=_cap;
}
_blank_sem.V();
}
private:
std::vector<T> _rq;
int _cap; // 环形队列上限
Sem _blank_sem; // 生产者信号量(blank:空白)
Sem _data_sem; // 消费者信号量
int _p_step;// 生产者步数
int _c_step;// 消费者步数
// 使用两把锁维护多生产多消费
// 使用queue模拟实现生产消费模型时,使用push和pop操作queue,临时资源是queue(队列本身)
// 因此,生产和消费必须是串行的,只能一者访问queue
// 但vector是通过下标访问数据的,所以生产者和消费者可以并行访问
// 所以使用两把锁,一把维护生产者,一把维护消费者
Mutex _pmutex; // 维护生产者
Mutex _cmutex; // 维护消费者
};
2.2 Sem.hpp:
封装 sem:
- 构造调用 sem_init 初始化信号量,可以传入参数指定信号量大小,不传默认是二元信号量。
- 析构调用 sem_destroy 销毁信号量。
- P 操作调用 sem_wait 对指定信号量的值减一。
- V 操作调用 sem_post 对指定信号量的值加一。
#pragma once
#include<semaphore.h>
namespace SemModule
{
const int defaultvalue=1;
class Sem
{
public:
Sem(const int value=defaultvalue)
{
// 0表示线程间共享,非0表示进程间共享
sem_init(&_sem,0,value);
}
// P操作,等待信号量,--
void P()
{
sem_wait(&_sem);
}
// V操作,发布信号量,++
void V()
{
sem_post(&_sem);
}
~Sem()
{
sem_destroy(&_sem);
}
private:
sem_t _sem;
};
}
更多推荐



所有评论(0)