目录

1. POSIX 信号量

1.1 初始化信号量

1.2 销毁信号量

1.3 等待信号量【P操作】

1.4 发布信号量【V操作】

2. 基于环形队列的生产消费模型

2.1 RingQueue.hpp:

2.2 Sem.hpp:


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:

要实现环形队列的生产消费模型,需要两个信号量分别控制生产者和消费者的操作。

此外,还需要两把互斥锁来分别管理:

  1. 生产者之间的互斥
  2. 消费者之间的互斥

这与之前使用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;
    };
}

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐