Linux线程同步与互斥(三):POSIX信号量与环形队列生产者消费者模型

在多线程编程中,我们经常遇到这样的场景:有多个相同类型的资源(比如一块内存被分成多个缓冲区、一个电影院有多个座位、一个环形队列有多个空位)。如果我们只用互斥锁,一次只能让一个线程访问整个资源池,效率很低。 更好的做法是:允许线程同时访问不同的子资源,只要它们不冲突。

信号量(Semaphore) 正是为此而生的同步工具。它像一个计数器,记录着当前可用资源的数量,线程在使用资源前必须先"预订"(P操作),用完后"归还"(V操作)。


一、信号量基本概念

1.1 信号量是什么

信号量是一个非负整数计数器,它表示某种资源的可用数量 。每个线程在访问资源前,必须先执行 P操作 (等待,减1),如果计数器为0则阻塞;访问结束后执行 V操作(释放,加1),并唤醒等待的线程。

1.2 为什么需要信号量

  • 互斥锁一次只允许一个线程进入临界区 ,适合保护整体资源(比如一个队列、一个变量)。

  • 信号量 :**允许多个线程同时访问不同部分的资源,只要资源数量足够。**例如:一个数组有10个槽位,最多允许10个生产者同时往不同槽位写数据。

信号量本质是对资源数量的预订机制:你不需要等到真正使用时才检查条件,而是在访问前就通过信号量"预订"了一份资源。

1.3 P/V 操作的原子性

  • **sem_wait(P):**将信号量值减1,如果结果 < 0 则阻塞。减1操作是原子的,不会被打断。

  • **sem_post(V):**将信号量值加1,如果有线程阻塞则唤醒一个。加1操作也是原子的

正是这种原子性,保证了多个线程同时申请资源时不会出现"超额预订"。

1.4 二元信号量 vs 互斥锁

  • 当**信号量初始值为 1 时,**它就成了二元信号量,功能与互斥锁完全一致:同一时刻只有一个线程能通过 P 操作。

  • 但互斥锁有所有权概念(只能由加锁的线程解锁),而信号量没有这个限制,任何线程都可以 V 操作。

二、环形队列生产者消费者模型

2.1 为什么用环形队列?

  • 环形队列(Ring Buffer)用 固定大小数组 + 模运算实现,空间利用率高,没有动态内存分配。

  • 使用信号量,我们可以优雅地解决这个问题------用两个信号量分别记录空位数量和数据数量

2.2 设计思想

  • 定义两个信号量:

    • sem_blank:初始值为队列容量 N,表示空位的个数。生产者每次生产前要 P(blank)。

    • sem_data:初始值为 0,表示已有数据的个数。消费者每次消费前要 P(data)。

  • 生产者和消费者各自维护自己的下标(p_stepc_step),移动时取模。

  • 关键结论:

    • 队列不为空且不为满时,生产者和消费者可以同时进行(因为它们操作不同位置)。

    • 队列为空或为满时,两者必须同步(生产者先走或消费者先走)。

    • 信号量自动保证了这些约定:空位为0时生产者阻塞,数据为0时消费者阻塞。

2.3 约定验证

约定 信号量如何保证
空时,生产者先运行 sem_data 为0,消费者 P(data) 阻塞,生产者 P(blank) 成功
满时,消费者先运行 sem_blank 为0,生产者 P(blank) 阻塞,消费者 P(data) 成功
生产者不能超过消费者一圈 sem_blank 最多为 N,当生产者领先一圈时,sem_blank 为0,生产者阻塞
消费者不能超过生产者 sem_data 最多为 N,当消费者领先时 sem_data 为0,消费者阻塞

三、POSIX 信号量接口

复制代码
#include <semaphore.h>

// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// pshared: 0 表示线程间共享,非0表示进程间共享
// value: 初始资源数量

// 销毁信号量
int sem_destroy(sem_t *sem);

// P操作:等待,减1
int sem_wait(sem_t *sem);

// V操作:发布,加1
int sem_post(sem_t *sem);

⚠️ 注意:sem_wait 可能被信号中断,通常我们忽略返回值或循环调用。

四、单生产单消费demo

4.1 Makefile

复制代码
ring_cp:Main.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f ring_cp

4.2 Sem.hpp

复制代码
#include <iostream>
#include <pthread.h>
#include <semaphore.h>

namespace SemModule
{
    const int defaultvalue = 1;
    class Sem
    {
    public:
        Sem(unsigned int sem_value = defaultvalue)
        {
            sem_init(&_sem, 0, sem_value);
        };
        void P()
        {
            int n = sem_wait(&_sem);
            (void)n;
        }
        void V()
        {
            int n = sem_post(&_sem);
            (void)n; // 原子的
        }

        ~Sem()
        {
            sem_destroy(&_sem);
        };

    private:
        sem_t _sem;
    };
}

4.3 Main.cc

复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "RingQueue.hpp"

void *consumer(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    while (true)
    {
        // 1.消费任务
        int t = 0;
        rq->Pop(&t);

        // 2.处理任务 -- 处理任务的时候,这个任务已经被我们拿到了线程上下文中了,不属于队列
        std::cout << "消费者拿到了一个数据" << t << std::endl;
    }
}

void *productor(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    int data = 1;
    while (true)
    {
        // sleep(2);

        // 1.获得任务
        std::cout << "生产了一个任务: " << data << std::endl;

        // 2.生产任务
        rq->Equeue(data);
        data++;
    }
}

int main()
{
    // 申请阻塞队列
    RingQueue<int> *rq = new RingQueue<int>();

    // 构建生产和消费者
    pthread_t c[1], p[1];

    pthread_create(c, nullptr, consumer, rq);
    pthread_create(p, nullptr, productor, rq);

    pthread_join(c[0], nullptr);
    pthread_join(p[0], nullptr);
    return 0;
}

4.4 RingQueue.hpp

复制代码
#pragma once

#include <iostream>
#include <vector>
#include "Sem.hpp"
using namespace SemModule;
static const int gcap = 5; // for debug

template <typename T>
class RingQueue
{
public:
    RingQueue(int cap = gcap)
        : _cap(cap),
          _rq(cap),
          _blank_sem(cap),
          _p_step(0),
          _data_sem(0),
          _c_step(0)
    {
    }
    void Equeue(const T &in)
    {
        // 生产者
        //1.申请信号量,空位置信号量
        _blank_sem.P();
        //2.生产
        _rq[_p_step] = in;
        //3.更新下标
        _p_step++;
        //4.维持环形特性
        _p_step %= _cap;
        _data_sem.V();
    }
    void Pop(T *out)
    {
        // 消费者
        //1.申请信号量,数据信号量
        _data_sem.P();
        //2.消费
        *out = _rq[_c_step];
        //3.更新下标
        ++_c_step;
        //4.维持环形特性
        _c_step %= _cap;
        _blank_sem.V();
    }
    ~RingQueue() {}

private:
    std::vector<T> _rq;
    int _cap;
    // 生产者 -- 关注空的资源
    Sem _blank_sem; // 空位置
    int _p_step;
    // 消费者
    Sem _data_sem; // 数据
    int _c_step;
};

生产者快 / 消费者慢

生产者慢 / 消费者快

生产者/ 消费者 同步

五、单生产单消费demo

在上面代码的基础上,修改了mutex.hpp 、main.cc 、RingQueue.hpp

5.1 Mutex.hpp

复制代码
#pragma once

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

namespace MutextModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
            (void)n;
        }
        void Unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
            (void)n;
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }
        pthread_mutex_t *Get()
        {
            return &_mutex;
        }
    private:
        pthread_mutex_t _mutex;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex) : _mutex(mutex)
        {
            _mutex.Lock();
        };
        ~LockGuard()
        {
            _mutex.Unlock();
        };

    private:
        Mutex &_mutex;
    };
}

5.2 main.cc

复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "RingQueue.hpp"

void *consumer(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    while (true)
    {
        // 1.消费任务
        int t = 0;
        rq->Pop(&t);

        // 2.处理任务 -- 处理任务的时候,这个任务已经被我们拿到了线程上下文中了,不属于队列
        std::cout << "消费者拿到了一个数据" << t << std::endl;
    }
}

void *productor(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *>(args);
    int data = 1;
    while (true)
    {
        // sleep(2);

        // 1.获得任务
        std::cout << "生产了一个任务: " << data << std::endl;

        // 2.生产任务
        rq->Equeue(data);
        data++;
    }
}

int main()
{
    // 申请阻塞队列
    RingQueue<int> *rq = new RingQueue<int>();

    // 构建生产和消费者
    pthread_t c[2], p[3];

    pthread_create(c, nullptr, consumer, rq);
    pthread_create(c + 1, nullptr, consumer, rq);
    pthread_create(p, nullptr, productor, rq);
    pthread_create(p + 1, nullptr, productor, rq);
    pthread_create(p + 2, nullptr, productor, rq);

    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);
    pthread_join(p[2], nullptr);
    return 0;
}

5.3 RingQueue.hpp

复制代码
#pragma once

#include <iostream>
#include <vector>
#include "Sem.hpp"
#include "Mutex.hpp"

using namespace SemModule;
using namespace MutextModule;
static const int gcap = 5; // for debug

template <typename T>
class RingQueue
{
public:
    RingQueue(int cap = gcap)
        : _cap(cap),
          _rq(cap),
          _blank_sem(cap),
          _p_step(0),
          _data_sem(0),
          _c_step(0)
    {
    }
    void Equeue(const T &in)
    {
        // 生产者
        // 1.申请信号量,空位置信号量
        _blank_sem.P();

        {
            LockGuard lockguard(_pmutex);
            //_pmutex.Lock();
            // 2.生产
            _rq[_p_step] = in;
            // 3.更新下标
            _p_step++;
            // 4.维持环形特性
            _p_step %= _cap;
            //_pmutex.Unlock();
        }

        _data_sem.V();
    }
    void Pop(T *out)
    {
        // 消费者
        // 1.申请信号量,数据信号量
        _data_sem.P();

        {
            LockGuard lockguard(_cmutex);
            //_cmutex.Lock();
            // 2.消费
            *out = _rq[_c_step];
            // 3.更新下标
            ++_c_step;
            // 4.维持环形特性
            _c_step %= _cap;
            // _cmutex.Unlock();
        }

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

private:
    std::vector<T> _rq;
    int _cap;
    // 生产者 -- 关注空的资源
    Sem _blank_sem; // 空位置
    int _p_step;
    // 消费者
    Sem _data_sem; // 数据
    int _c_step;

    // 维护多生产多消费,2把锁
    Mutex _cmutex;
    Mutex _pmutex;
};
相关推荐
未来转换2 小时前
基于A2A协议的生产应用实践指南(Java)
java·开发语言·算法·agent
Rust语言中文社区2 小时前
【Rust日报】Clone:像进程一样 fork 虚拟机的 Rust KVM VMM
开发语言·后端·rust
求知也求真佳2 小时前
S02|工具使用:让 Agent 真正会干活,添加工具
开发语言·agent
Dwzun2 小时前
基于Java+SpringBoot+Vue的校园二手物品置换系统设计与实现【附源码+文档+部署视频+讲解】
java·开发语言·spring boot
ic爱吃蓝莓2 小时前
破译 Linux 的“黑话”(常用命令解释)
linux·运维·服务器
云动课堂2 小时前
【运维实战】企业级VSFTPD 文件服务 · 一键自动化部署方案 (适配银河麒麟 V10 /openEuler /CentOS)
运维·centos·自动化
charlie1145141912 小时前
嵌入式Linux驱动开发(3)——内核模块机制 - Linux 的插件系统
linux·运维·开发语言·驱动开发·嵌入式硬件·学习
Elastic 中国社区官方博客2 小时前
使用 Remote Write 将 Prometheus 指标发送到 Elasticsearch
大数据·运维·elasticsearch·搜索引擎·全文检索·prometheus
C、空白格2 小时前
Java集成Vosk实现离线语音识别
java·开发语言·语音识别