【Linux笔记】——线程同步信号量与环形队列生产者消费者模型的实现(PV操作)

🔥个人主页🔥:孤寂大仙V
🌈收录专栏🌈:Linux
🌹往期回顾🌹:【Linux笔记】——线程同步条件变量与生产者消费者模型的实现
🔖流水不争,争的是滔滔不息


一、POSIX信号量

POSIX信号量是线程/进程间同步的高效工具,通过计数器机制控制对共享资源的访问。用于同步操作,达到无冲突的访问共享资源的目的。

初始化信号

int sem_init(sem_t *sem, int pshared, unsigned int value);

pshared:0表示线程间共享,非0表示进程间共享。
value:表示信号量初始值。

销毁信号量

int sem_destroy(sem_t *sem);

等待信号量

P操作

int sem_wait(sem_t *sem);

等待信号量会将信号量的值减1。

发布信号量

V操作

int sem_post(sem_t *sem);

发布信号量,表示资源使用完毕,可以归还资源了。将信号量的值加1。

二、信号量的封装

#include <iostream>
#include <pthread.h>
#include <semaphore.h>

using namespace std;
const int num = 5;
namespace SemModule
{
    class Sem
    {
    public:
        Sem(int sem_value = num)
        {
            sem_init(&_sem, 0, sem_value); // 初始化信号量
        }
        void P()
        {
            int n = sem_wait(&_sem); // 等待信号
        }
        void V()
        {
            int n = sem_post(&_sem); // 发布信号
        }
        ~Sem()
        {
            sem_destroy(&_sem); // 销毁信号量
        }

    private:
        sem_t _sem;
    };
}

对POSIX信号量函数进行封装,等待信号就是p操作,发布信号就是v操作。

三、基于环形队列的生产者消费者模型

在这里插入图片描述

整个唤醒队列为空,生产者先运行,环形队列为满,消费者先运行。生产者不能把消费者套一个圈。消费者,不能超过生产者。上面这些既是约点又是必须满足的条件。

在这个唤醒队列中,生产者消费者只要不访问同一个位置就可以同步运行,比如生产者生产了五个位置的资源,那么消费者从起点位置开始肯定可以开始消费了。也就是说明了环形队列里的内容只要不为空或者不为满就可以同时运行了。环形队列为空的话,生产者消费者线程只能互斥,这是只有生产者去运行,生产资源。环形队列为满的话只能,生产者消费者也是只能互斥,消费者先运行,去消费资源。


环形队列

#pragma once
#include <iostream>
#include <pthread.h>
#include <semaphore.h>
#include <vector>
#include "Sem.hpp"
#include "Mutex.hpp"

using namespace std;
using namespace MutexModule;
using namespace SemModule;

const int gcap = 5;

template <class T>
class Semqueue//
{
public:
    Semqueue(int cap = gcap)
        : _cap(cap)
        , _v(cap)
        , _blank_sem(cap)   //空位置,一开始环形队列空格子多少空位置多少
        , _data_sem(0)
        , _p_step(0)
        , _c_step(0)
    {
    }

    void Equeue(const T &in) //生产者
    {
        _blank_sem.P(); // 空位置--

        {
            LockGuard lockguard(_pmutex); // RAII自动上锁解锁
            _v[_p_step] = in;             // 放入数据
            _p_step++;                    // 下标移动
            _p_step %= _cap;              // 环形队列回到开头
        }
        _data_sem.V();// 数据++
    }

    void pop(T* out) //消费者
    {
        _data_sem.P();

        {
            LockGuard lockguard(_cmutex);
            *out=_v[_c_step];
            _c_step++;
            _c_step%=_cap;
        }

        _blank_sem.V();
    }
    ~Semqueue()
    {

    }

private:
    vector<T> _v;
    int _cap;
    // 生产者
    Sem _blank_sem; // 空位
    int _p_step;    // 下标
    Mutex _pmutex;
    // 消费者
    Sem _data_sem; // 数据
    int _c_step;   // 下标
    Mutex _cmutex;
};

成员变量
_blank_sem表示生产者的空位,_data_sem表示消费者的资源数据。_p_step表示生产者的下标,_c_step表示消费者的下标。这里的两把所后面说。_cap唤醒队列的大小。

成员函数生产者消费者代码

void Equeue(const T &in) //生产者
    {
        _blank_sem.P(); // 空位置--

        {
            LockGuard lockguard(_pmutex); // RAII自动上锁解锁
            _v[_p_step] = in;             // 放入数据
            _p_step++;                    // 下标移动
            _p_step %= _cap;              // 环形队列回到开头
        }
        _data_sem.V();// 数据++
    }

    void pop(T* out) //消费者
    {
        _data_sem.P();

        {
            LockGuard lockguard(_cmutex);
            *out=_v[_c_step];
            _c_step++;
            _c_step%=_cap;
        }

        _blank_sem.V();
    }

生产者,先P操作,这里的p操作(站着生产者的角度,生产者想要的是空位)可以理解为空位置–,在环形队列中是被生产者占用了。然后放入数据移动下标,因为是队列为了回到开头_p_step%=_cap。最后V操作,相当于消费者能消费的资源++。
消费者,P操作,(站在消费者的角度,消费者想要的是生产者生产的资源)可以理解为消费者的资源数据–,然后执行消费逻辑,最后资源消费完了,唤醒队列的空位++。

极端条件下,生产者把整个环形队列的空位都生产了资源,那么此时消费者p操作就是使资源–消费资源,最后消费一个资源使空位++。现实中,生产者消费者线程是并发执行的,生产者只要生产了资源消费者就能消费,PV 操作也就实现了生产者消费者的并发执行。注意以上是站在理解的角度阐述的。

在这里插入图片描述


PV操作的本质

PV操作的本质是一种原子操作+阻塞队列的组合机制,用来控制多个线程/进程对共享资源的并发访问。
P是尝试获取资源,如果资源不足,当前线程阻塞,加入等待队列。
V是释放资源,如果线程在等,唤醒一个线程,否则就增加资源计数。

其实,PV操作的底层在功能上等价于“互斥锁+环境变量”,是打包好的,统一封装的一套同步机制。
但是PV操作的同步与互斥机制,颗粒度更粗,用锁和条件变量颗粒度更细。

PV 是对“互斥 + 等待/唤醒”的封装,它以资源计数为中心,自动处理线程阻塞与唤醒;而 mutex + cond 提供了更基础、更灵活但更复杂的控制方式。

对于上述概念要有所甄别,信号量(PV操作)可以实现同步,也可以实现互斥,但本身是一个更底层更通用的原语。你用它去做“同步”就是同步;你用它去做“互斥”它也可以做互斥。 但是同步和互斥是两个逻辑概念,信号量只是实现它们的工具


上面代码,发现我们在p操作之后加锁,为的是多个线程的情况下,防止多个线程同时访问共享资源。P 操作是在做“资源同步”控制,mutex 是在做“数据互斥”控制。P() 的核心作用是:"判断条件是否满足,不满足就阻塞。所以需要在p后面加锁,防止多线程同时访问共享资源。

P():检查是否还有空箱子可用。如果没有空箱子了(信号量为0),你就得在旁边等着(同步阻塞)。
上锁:获取这个箱子的使用权。即便有箱子,也不能让两个人同时装一个箱子(互斥)。

四、pv操作与条件变量

条件变量的同步过程
是基于线程先拿到锁,然后进行判断,不满足就同步机制等待。
加锁->判断条件(队列是否为空)->同步(自动解锁+阻塞+等唤醒后重新加锁)->条件满足后操作共享资源->解锁。

线程进来先互斥然后同步。
信号量同步过程
基于资源数量判断,是否可以进入,不够就自动阻塞。
p操作(资源够了吗?不够就阻塞(同步))->资源够就直接进入临界区->加锁保护共享数据不被并发访问->v操作释放资源。

先同步后互斥


多线程pv操作生产者消费者模型源码

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孤寂大仙v

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值