hello~ 很高兴见到大家! 这次带来的是Linux系统中关于线程这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页 : 默|笙

文章目录
- 一、POSIX信号量
-
- [1.1 快速认识信号量接口](#1.1 快速认识信号量接口)
-
- [1. 信号量接口](#1. 信号量接口)
- [2. 接口封装](#2. 接口封装)
- [1.2 基于环形队列的生产者消费者模型](#1.2 基于环形队列的生产者消费者模型)
-
- [1. 介绍](#1. 介绍)
- [2. 代码实现](#2. 代码实现)
一、POSIX信号量
- POSIX 信号量的用法与 SystemV 信号量的作用相同,都是用于实现同步操作达到无冲突的访问共享内存的目的。不过 POSIX 信号量可以实现线程之间的同步操作。
- 信号量的本质就是一种对临界资源的计数与预定机制,它通过一个非负整数记录可用资源数量,线程 / 进程通过P、V 操作申请或释放资源,从而实现同步与互斥,安全地访问共享资源。
1.1 快速认识信号量接口
1. 信号量接口

- sem_init 是用来初始化信号量的接口,第一个参数需要我们传递一个 sem_t 类型变量的地址,第二个参数用于指定信号量是线程共享还是进程共享(0 表示线程间共享,非 0 表示进程间共享),第三个参数则是设置这个信号量的初始值,代表能同时访问资源的线程 / 进程数量。
- 使用 POSIX 信号量相关接口(如 sem_init、sem_wait、sem_post 等),需要包含头文件 <semaphore.h>。

- sem_destroy 是用来销毁信号量的接口,只需要传递目标信号量的地址,即可完成销毁。

- sem_wait 是用来减少信号量的接口,它会让传递进去的信号量的值 -1 ,也就是P 操作。
如果信号量值为 0,sem_wait 会阻塞等待,直到信号量大于 0 才继续。

- sem_post 是用来增加信号量的接口,它会让传递进去的信号量的值**+1**,也就是V 操作。如果当前有线程因信号量为 0 而阻塞在 sem_wait 上,sem_post 会唤醒其中一个等待的线程。
2. 接口封装
cpp
#pragma once
#include <iostream>
#include <semaphore.h>
class Sem
{
public:
Sem(int init_val)
{
if (init_val >= 0)
{
int n = sem_init(&_sem, 0, init_val);
(void)n;
}
}
void P()
{
int n = sem_wait(&_sem);
(void)n;
}
void V()
{
int n = sem_post(&_sem);
(void)n;
}
~Sem()
{
int n = sem_destroy(&_sem);
(void)n;
}
private:
sem_t _sem;
};
1.2 基于环形队列的生产者消费者模型
1. 介绍

- 之前我们介绍过的阻塞队列,它的临界资源是整块独占使用的 ------ 一个线程要么不使用,要么就直接占据整个队列资源。那我们能不能把资源拆分成多个独立单元,让多个线程可以更细粒度地、同时访问不同部分的资源呢?基于这种思路,环形队列就应运而生了。
- 环形队列将一份临界资源拆分为多个单元,通过头指针(head)与尾指针(tail)来标识位置。通常约定,尾指针指向最后一个有效元素的下一个空位置。但这样一来会出现一个经典问题:队列为空和队列为满时,头、尾指针的条件都是 head == tail,这会导致无法区分两种状态。
- 为此通常有两种经典解决方案:
- 增设计数器:记录当前队列中实际存储的元素数量,通过计数器数值直接判断空 / 满。
- 牺牲一个空位:刻意舍弃队列的最后一个位置不用,将 tail 的下一个位置等于 head((tail + 1) % capacity == head)作为队列满的判定条件。
- 但是因为有信号量的存在,我们可以很轻松地解决这个问题。队列里的每一份资源都是独立的临界资源,同一时间只能有一个线程访问,因此信号量可以完美替代计数器的角色。生产者消费者模型中有两个角色:生产者和消费者。如果把生产者看作往队列里放苹果(数据),消费者就是从队列里取苹果。我们可以设置两个信号量:一个表示苹果的个数,另一个表示空位的个数。对于生产者来说,空位才是资源;对于消费者来说,苹果才是资源。生产者或消费者想要操作,必须先通过信号量进行资源 "预定"(类似买票),才能继续执行。
- 至于具体操作哪个位置,由 head 和 tail 指针明确标识。在队列既不为空也不为满时,head 永远指向有数据的位置,tail 永远指向下一个空位置;生产者线程在 tail 指针处插入数据,消费者线程在 head 指针处获取数据,两个角色可以同时访问不同位置。同一类角色之间需要互斥与同步,不同角色之间只需要同步,不需要互斥。当队列为空时,消费者等待,由生产者投放数据;当队列满时,生产者等待,由消费者取走数据,从而实现线程间的互斥与同步。
- 环形队列的运作就像一场追逃游戏:消费者线程始终在追逐生产者线程。它能保证同一时刻:最多一个生产者在写入、最多一个消费者在读取,二者可以并发操作,互不冲突。
2. 代码实现
- 环形队列的实现不采用链表,而是基于数组实现;通过下标对数组容量取模(%) 即可模拟环形访问。基于数组实现是为了更高的访问效率、更好的 CPU 缓存、更低的内存开销,以及更简单的实现。
- 信号量的 P/V 操作用于实现线程间的同步,本身由操作系统保证原子性,无需锁保护。将它们放在互斥锁外部,仅用锁保护临界资源的访问,能最小化锁粒度、缩短锁持有时间,大幅提高多线程并发性能。
cpp
#pragma once
#include <iostream>
#include "Sem.hpp"
#include "Mutex.hpp"
const int capdefault = 10;
template <class T>
class RingQueue
{
public:
RingQueue(int cap = capdefault)
: _cap(cap)
, _rq(cap)
, _data_sem(0)
, _blank_sem(cap) // 最开始队列肯定都是空格
, _consumer_step(0)
, _productor_step(0)
{
}
// 往队列里面放数据
void Enqueue(const T &in)
{
//申请空格资源
_blank_sem.P();
{
LockGuard lockguard(_pro_lock);
_rq[_productor_step++] = in;
_productor_step %= _cap;
}
//释放数据资源
_data_sem.V();
}
void Pop(T *out)
{
//申请数据资源
_data_sem.P();
{
LockGuard lockguard(_con_lock);
*out = _rq[_consumer_step++];
_consumer_step %= _cap;
}
//释放空格资源
_blank_sem.V();
}
~RingQueue()
{
}
private:
int _cap; // 队列容量
std::vector<T> _rq; // 队列本体
Sem _data_sem; // 数据数量
Sem _blank_sem; // 空格数量
int _consumer_step; // 消费者从哪里拿数据
int _productor_step; // 生产者在哪里放数据
Mutex _con_lock; // 消费者之间实现互斥的锁
Mutex _pro_lock; // 生产者之间实现互斥的锁
};
今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!