【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; // 生产者之间实现互斥的锁
};

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

相关推荐
cui_ruicheng2 小时前
Linux IO入门(一):从C语言IO到文件描述符
linux·运维·c语言
丸子家的银河龙2 小时前
yocto使用实例[1]-自定义内核配方
linux
北京耐用通信2 小时前
工业通信升级:耐达讯自动化CAN转EtherCAT网关的高效落地方案
服务器·人工智能·科技·物联网·自动化·信息与通信
青花瓷2 小时前
ubuntu22.04的ibus中文输入法的安装
运维·ubuntu
Wenweno0o2 小时前
CC-Switch & Claude 基于 Linux 服务器安装使用指南
linux·服务器·claude code·cc-switch
志栋智能2 小时前
当巡检遇上超自动化:一场运维质量的系统性升级
运维·服务器·网络·数据库·人工智能·机器学习·自动化
主角1 72 小时前
Keepalived高可用与负载均衡
运维·负载均衡
星辰徐哥2 小时前
CDN工作原理:节点缓存、智能调度,减少跨网传输延迟
服务器·缓存·php
Fanfanaas2 小时前
Linux 系统编程 进程篇(一)
linux·运维·服务器·c语言·开发语言·网络·学习