【Linux】多线程——信号量

信号量

信号量的原理

多线程访问临界资源时,为了实现互斥。通常就是加锁,但是加锁就会导致并行访问转变为串行访问,减小访问的效率。

我们在访问临界资源时,既希望保护好临界资源,又希望保证是并行的去访问(效率不降低)。

我们将临界资源分区,提前为执行流分配对应的空间,只有得到分配空间的执行流才能访问资源。直到空间被归还。

  • 这是信号量的基本思路。信号量就是一把计数器。只有提前申请信号量的执行流才能访问临界区。
  • 信号量常见的操作是P(申请空间),V(释放空间)。
  • P操作在计数器大于0时,就--计数器,如果为0,就阻塞等待。V操作将计数器++;

信号量的概念

Linux信号量(Semaphore)是一种实现线程或者进程同步和互斥的机制。本质就是一把计数器

每个执行流必须先P操作申请信号量,执行完任务后V操作释放信号量。

信号量的PV操作是原子的!

因为信号量会被所有的线程看到,属于临界资源。因为对于临界资源要做到互斥,实现的方式就是申请信号量和释放信号量在汇编的一条指令,不会因为CPU调度破坏结果。

申请信号量失败(计数器为0)就会被阻塞等待,直到有执行流释放信号量后,线程被唤醒去获得信号量。因此在信号量的结构中还包含等待队列

除此之外,还有标记信号量的状态等。

struct sem
{
    int count; //计数器
    wait_queue;//等待队列
    status;    //标记状态
}

信号量接口

  • 初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);

sem:传入信号量;pshared:0为默认线程间共享;value为计数器的初值。

  • 销毁
int sem_destroy(sem_t *sem);
  • 申请信号量
int sem_wait(sem_t *sem);

申请成功返回0,失败阻塞等待返回-1

  • 释放信号量
int sem_post(sem_t *sem);
  • 成功返回0,失败返回-1

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

基于环形队列的生产消费模型,是一种经典的并发设计模式。它解决了生产者和消费者互斥同步的问题。生产者负责生产数据并放到队列中,消费者负责从队列中取出数据,并且消费数据。环形队列作为缓冲区。

环形队列是一种先进先出的线性数据结构。与普通队列相比,环形队列首尾相接,是固定长度,减少内存碎片化。

环形队列是由数组模拟实现的

对于信号量的模型,p和c代表生产者和消费者。

p\c起始指向同一位置,每次p申请一个信号量p往前走一步,但不可先去c一圈

如果c滞后于p,那么c往前走,取出数据,释放信号量。但是c不得先于p。

必须遵守的俩个规则

生产者和消费者不能对同一位置进行访问。

  • 因为无法确定是先放数据,还是取数据。所以同一位置的时候,必须是互斥的。
  • 产生同一位置的情况:全空或者全满(全满,就不能申请信号量,不需要特殊处理)

  • 生产者不能生产数据超过消费者一轮

如果超过,已有的数据没有来得急消费,就会被覆盖

  • 消费者不会先于生产者

因为没有数据可供消费。


设计简单环形队列消费者生产模型

框架:

  • Ringqueue的环形队列的底层是vector,成员包含size记录起始的空间;
  • 还需要标记p 和c 的当前位置
  • 需要空间信号量和数据信号量。分别给生产者和消费者。
template<class T>
class RingQueue
{
public:
    RingQueue()
    {
        //
    }

    void Push()
    {//}

    void Pop()
    {//}   

    ~RingQueue()
    {//}

private:
    std::vector<T> _ringqueue;
    int _size;

    int _c_step;
    int _p_step;

    sem_t _space_sem;
    sem_t _data_sem;
};

Push和Pop

生产者生产数据和消费者消费数据。都必须先申请信号量;

    void Push(const T &in)
    {
        P(_space_sem);
        _ringqueue[_p_step] = in;
        _p_step++;
        _p_step %= _size;

        V(_data_sem);
    }

    void Pop(T *out)
    {
        P(_data_sem);
        *out = _ringqueue[_c_step];
        _c_step++;
        _c_step %= _size;

        V(_space_sem);
    }

P和V

对于的就是让计数器++和--

P操作调用接口wait,V操作调用post

    void P(sem_t &sem)
    {
        sem_wait(&sem);
    }

    void V(sem_t &sem)
    {
        sem_post(&sem);
    }

这个模型优点在哪里?

  • 实现生产和消费的解耦;生产者只需要关注生产,将生产好的数据放到队列中。消费者需要要取出数据来消费。
  • 高度并发。只要当p和c位于同一位置,即生产者和消费者谁先放数据的时候,需要互斥判断。其它时候都是正常的。相比于阻塞队列并发程度更高。
  • 空间利用率更高。信号量的设计,提前将空间预定。
  • 生产者的生产数据,放数据,消费者的取数据,消费者的消费数据。绝大数都是并发的。

多生产对多消费

基于上面实现的单生产但消费。如果想要改成多生产多消费,必然会遇到问题。

信号量的申请是不受影响的。但是p和c的当前位置,就会因为线程竞争受到影响,必须加锁保护。

先申请信号量还是加锁?

为了能够最大实现性能,先申请信号量。

如果先申请锁,那么即使有空间也要等待一个执行流结束,再去申请锁,再申请信号量。

如果先申请信号量,那么锁一结束,就可以立马再申请锁,执行任务。效率更高。


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

#pragma once
#include <vector>
#include <semaphore.h>

#include "LockGuard.hpp"

const int defaultsize = 5;

template <class T>
class RingQueue
{
private:
    void P(sem_t &sem)
    {
        sem_wait(&sem);
    }

    void V(sem_t &sem)
    {
        sem_post(&sem);
    }

public:
    RingQueue(int size = defaultsize)
        : _size(size), _p_step(0), _c_step(0), _ringqueue(size)
    {
        pthread_mutex_init(&_p_mutex, nullptr);
        pthread_mutex_init(&_c_mutex, nullptr);
        sem_init(&_space_sem, 0, size);
        sem_init(&_data_sem, 0, 0);
    }

    void Push(const T &in)
    {
        P(_space_sem);
        {
            LockGuard lockguard(&_p_mutex);
            _ringqueue[_p_step] = in;
            _p_step++;
            _p_step %= _size;
        }
        V(_data_sem);
    }

    void Pop(T *out)
    {
        P(_data_sem);
        {
            LockGuard lockguard(&_c_mutex);
            *out = _ringqueue[_c_step];
            _c_step++;
            _c_step %= _size;
        }
        V(_space_sem);
    }

    ~RingQueue()
    {
        pthread_mutex_destroy(&_p_mutex);
        pthread_mutex_destroy(&_c_mutex);

        sem_destroy(&_space_sem);
        sem_destroy(&_data_sem);
    }

private:
    std::vector<T> _ringqueue;
    int _size;

    int _c_step;
    int _p_step;

    pthread_mutex_t _p_mutex;
    pthread_mutex_t _c_mutex;
    sem_t _space_sem;
    sem_t _data_sem;
};

  • 26
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

深度搜索

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

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

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

打赏作者

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

抵扣说明:

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

余额充值