【Linux】线程互斥与同步_同步(2)_环形队列

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


文章目录

  • 一、POSIX信号量
    • [1.1 快速认识信号量接口](#1.1 快速认识信号量接口)
      • [1. 信号量接口](#1. 信号量接口)
      • [2. 接口封装](#2. 接口封装)
    • [1.2 基于环形队列的生产者消费者模型](#1.2 基于环形队列的生产者消费者模型)
      • [1. 介绍](#1. 介绍)
      • [2. 代码实现](#2. 代码实现)

一、POSIX信号量

  1. POSIX 信号量的用法与 SystemV 信号量的作用相同,都是用于实现同步操作达到无冲突的访问共享内存的目的。不过 POSIX 信号量可以实现线程之间的同步操作。
  2. 信号量的本质就是一种对临界资源的计数与预定机制,它通过一个非负整数记录可用资源数量,线程 / 进程通过P、V 操作申请或释放资源,从而实现同步与互斥,安全地访问共享资源。

1.1 快速认识信号量接口

1. 信号量接口

  1. sem_init 是用来初始化信号量的接口,第一个参数需要我们传递一个 sem_t 类型变量的地址,第二个参数用于指定信号量是线程共享还是进程共享(0 表示线程间共享,非 0 表示进程间共享),第三个参数则是设置这个信号量的初始值,代表能同时访问资源的线程 / 进程数量。
  2. 使用 POSIX 信号量相关接口(如 sem_init、sem_wait、sem_post 等),需要包含头文件 <semaphore.h>。
  1. sem_destroy 是用来销毁信号量的接口,只需要传递目标信号量的地址,即可完成销毁。
  1. sem_wait 是用来减少信号量的接口,它会让传递进去的信号量的值 -1 ,也就是P 操作。
    如果信号量值为 0,sem_wait 会阻塞等待,直到信号量大于 0 才继续。
  1. 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. 介绍

  1. 之前我们介绍过的阻塞队列,它的临界资源是整块独占使用的 ------ 一个线程要么不使用,要么就直接占据整个队列资源。那我们能不能把资源拆分成多个独立单元,让多个线程可以更细粒度地、同时访问不同部分的资源呢?基于这种思路,环形队列就应运而生了。
  2. 环形队列将一份临界资源拆分为多个单元,通过头指针(head)与尾指针(tail)来标识位置。通常约定,尾指针指向最后一个有效元素的下一个空位置。但这样一来会出现一个经典问题:队列为空和队列为满时,头、尾指针的条件都是 head == tail,这会导致无法区分两种状态。
  3. 为此通常有两种经典解决方案:
  1. 增设计数器:记录当前队列中实际存储的元素数量,通过计数器数值直接判断空 / 满。
  2. 牺牲一个空位:刻意舍弃队列的最后一个位置不用,将 tail 的下一个位置等于 head((tail + 1) % capacity == head)作为队列满的判定条件。
  1. 但是因为有信号量的存在,我们可以很轻松地解决这个问题。队列里的每一份资源都是独立的临界资源,同一时间只能有一个线程访问,因此信号量可以完美替代计数器的角色。生产者消费者模型中有两个角色:生产者和消费者。如果把生产者看作往队列里放苹果(数据),消费者就是从队列里取苹果。我们可以设置两个信号量:一个表示苹果的个数,另一个表示空位的个数。对于生产者来说,空位才是资源;对于消费者来说,苹果才是资源。生产者或消费者想要操作,必须先通过信号量进行资源 "预定"(类似买票),才能继续执行。
  2. 至于具体操作哪个位置,由 head 和 tail 指针明确标识。在队列既不为空也不为满时,head 永远指向有数据的位置,tail 永远指向下一个空位置;生产者线程在 tail 指针处插入数据,消费者线程在 head 指针处获取数据,两个角色可以同时访问不同位置。同一类角色之间需要互斥与同步,不同角色之间只需要同步,不需要互斥。当队列为空时,消费者等待,由生产者投放数据;当队列满时,生产者等待,由消费者取走数据,从而实现线程间的互斥与同步。
  3. 环形队列的运作就像一场追逃游戏:消费者线程始终在追逐生产者线程。它能保证同一时刻:最多一个生产者在写入、最多一个消费者在读取,二者可以并发操作,互不冲突。

2. 代码实现

  1. 环形队列的实现不采用链表,而是基于数组实现;通过下标对数组容量取模(%) 即可模拟环形访问。基于数组实现是为了更高的访问效率、更好的 CPU 缓存、更低的内存开销,以及更简单的实现。
  2. 信号量的 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; // 生产者之间实现互斥的锁
};

今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!

相关推荐
A小辣椒4 小时前
TShark:Wireshark CLI 功能
linux
A小辣椒8 小时前
TShark:基础知识
linux
AlfredZhao10 小时前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao1 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334661 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪1 天前
linux 拷贝文件或目录到指定的位置
linux
大树882 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠2 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质2 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush42 天前
嵌入式linux学习记录十四、术语
linux·嵌入式