Linux —— 信号量

目录

[1. 信号量的引入](#1. 信号量的引入)

[1.1 信号量的概念](#1.1 信号量的概念)

[1.2 本质的认识:](#1.2 本质的认识:)

[1.3 操作上的认识:](#1.3 操作上的认识:)

[2. 信号量的接口](#2. 信号量的接口)

[3. 321 CP场景](#3. 321 CP场景)

[3.1 理解环形队列](#3.1 理解环形队列)

[3.2 单生产者,单消费者的场景:](#3.2 单生产者,单消费者的场景:)

[3.2.1 理解](#3.2.1 理解)

[4. 代码实现](#4. 代码实现)

[4.1 version1 -- 单生产,单消费](#4.1 version1 -- 单生产,单消费)

[4.2 version2 -- 多生产,多消费](#4.2 version2 -- 多生产,多消费)


1. 信号量的引入

1.1 信号量的概念

  • 信号量的理论在之前的进程的文章中提过。
  • 在之前写的阻塞队列,生产消费模型的交易场所的那个队列是当作整体使用的,无法对一个队列进行任意地址访问,也是很难做,因为里面的数据结构不是很清楚到底是什么,但是可以将资源作为整体使用,之前举的例子:ATM机。
  • 将大块的资源拆分成一个一个的小块的资源,允许不同的线程访问同一个整体资源的不同部分,不就相当于允许多线程并发进入一个公共的资源。在使用共享资源,就可以分为两类:整体使用或者是不整体使用。
  • 整体使用就得有互斥能力,对应的就是互斥锁的技术。局部使用的场景就是:电影院。资源被局部使用的时候,最怕的就是两个问题:

一:一共10个资源,但是放入的线程数量是大于10的;
二:一共有10个资源,放入10个线程,但是让其中的两三个线程访问到了同一个资源

如果能够规避上面的两个问题,那么就可以让多线程并发访问,整体资源当中的一部分!!!

1.2 本质的认识:

1.3 操作上的认识:

信号量是一把计数器,跟一个整数一样,申请就--,释放就++。就可以将信号量理解成一个计数器,内部再加上一个锁,把信号量当成一个结构体,在结构体里有包含mutex互斥琐,再加一个对应的整数就可以了。

多线程访问,都得先申请信号量。但是前提是:都得先看到同一份信号量 - > 信号量本身就是 共享 / 临界资源 -> 信号量是为了保证公共资源的安全的 -> 所以PV操作要保证原子性!!!所以PV操作必须被做成原子的!!操作层面上的特性决定的。

2. 信号量的接口

sem_init():信号量的初始化

  • 头文件:<semaphore.h>
  • 信号量数据类型:sem_t
  • 推荐sem_init函数来进行初始化
  • pshared:表明进程间使用还是线程间使用,一般,0表示线程间使用,设置为0
  • value:信号量是一个计数器,表明初始值是多少
  • 返回值:sem_init() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error。成功返回0,失败返回-1,错误码被设置
  • sem_t sem n:定义多个信号量

sem_destroy():信号量的销毁

  • sem:将 sem_init() 刚刚创建好的信号量传递进来
  • 返回值:sem_destroy() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error.

sem_wait():减少信号量的值,对应的就是P操作

sem_post():释放一个信号量,计数器增加,对应的就是V操作

多进程的使用信号量:(了解)

直接用指针指向共享内存开头的位置,将该指针直接强转成信号类型,然后初始化,然后destroy,瞬间让两个进程可以用指针指向同一个共享内存的位置,把那一段共享内存初始化为信号量,被进程间使用。

3. 321 CP场景

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

3.1 理解环形队列

但是之后的操作不用再来判断为空为满的情况,因为信号量本质就是一个计数器。

3.2 单生产者,单消费者的场景:

3.2.1 理解

其实,如果第一次为满的话,后面都得等消费一个再生产一个,消费一个生产一个,就变成了串行执行,效率是会下降的!!!但是消费一个生产一个这样的步调不是主流。

4. 代码实现

4.1 version1 -- 单生产,单消费

用代码完成以上逻辑

cpp 复制代码
//Sem.hpp

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

//两个信号量

class Sem
{
public:
    Sem(int num):_initnum(num)
    {
        sem_init(&_sem, 0, _initnum);
    }
    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;
    int _initnum;
};
cpp 复制代码
//RingQueue.hpp
#pragma once

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

static int gcap = 5;

template <typename T>
class RingQueue
{
public:
    RingQueue(int cap = gcap)
        : _cap(cap), _ring_queue(cap), _space_sem(cap), _data_sem(0), _p_step(0), _c_step(0)
    {
    }
    void Pop(T *out)
    {
        // 先申请数据资源
        _data_sem.P();
        // 走到下面,要么是为满的情况,要么是不为空不为满的情况,不用担心生产者影响到自己
        *out = _ring_queue[_c_step++];
        _c_step %= _cap;
        _space_sem.V();
    }
    void Enqueue(const T &in)
    {
        // 1. 先申请空间资源
        _space_sem.P();
        // 生产数据,有空间,在哪里呀?
        _ring_queue[_p_step++] = in;
        // 单生产单消费中,生产者生产的时候,消费者不能来进行打扰,因为,进入到生产中,一定不为满,所有就只有为空或者是不为空不为满的情况
        // 不为空不为满二者可以并发执行,消费者并不影响生产者,为空的时候,生产者在运行期间,消费者的数据计数器为0,消费者不可能进入,就不会影响
        // 所以单生产单消费中,申请到了信号量我在放数据的时候没有人能影响到我
        // 维持环形特点
        _p_step %= _cap;
        _data_sem.V();
    }
    ~RingQueue()
    {
    }

private:
    std::vector<T> _ring_queue; // 临界资源,环形队列的底层逻辑
    int _cap;                   // 总容量大小,进行模运算

    Sem _space_sem; // 空间资源,空间信号量 - 生产者关心
    Sem _data_sem;  // 数据资源,信号量是计数器,数据资源的数量

    // 生产和消费的位置
    int _p_step;
    int _c_step;
};
cpp 复制代码
//main.cc
#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>

void *consumer(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *> (args);
    while(true)
    {
        sleep(1);
        int data = 0;
        rq->Pop(&data);  //拿数据
        std::cout << "消费了一个数据:" << data << std::endl;
    }
}

void *productor(void *args)
{
    RingQueue<int> *rq = static_cast<RingQueue<int> *> (args);
    int data = 1;
    while(true)
    {
        rq->Enqueue(data);   //入数据
        std::cout << "生产了一个数据:" << data << std::endl;
        data++;
    }
}

int main()
{
    RingQueue<int> *rq = new RingQueue<int>();
    pthread_t c,p;

    pthread_create(&c, nullptr, consumer, (void*)rq);
    pthread_create(&p, nullptr, productor, (void*)rq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);

    delete rq;
    return 0;
}

运行结果:

问题1:上面的代码没有加锁呀!!!能保证数据的安全吗??

使用信号量,变相的完成了为空和为满的同步和互斥动作,不为空不为满指向的是不同的位置,访问的就是不同的位置。

问题2:怎么没有发现,在临界区内部,没有判断资源是否就绪啊??

是因为申请信号量是对资源的预定机制,对信号量P操作的时候,虽然是申请信号量,但是本质,就是对资源是否就绪进行判断!!!申请信号量是对资源的预定机制是在判断:从临界区内,转移到了临界区外部(或者入口处)

问题3:整体访问(用互斥锁),局部访问(用信号量)

代码中:

cpp 复制代码
void Pop(T *out)
    {
        _data_sem.P();
        *out = _ring_queue[_c_step++];
        _c_step %= _cap;
        _space_sem.V();
    }
    void Enqueue(const T &in)
    {
        _space_sem.P();
        _p_step %= _cap;
        _data_sem.V();
    }

最开始的时候,_data_sem.P(); 值为0,_space_sem.P();为10,申请数据信号量申请不到,不就相当于数据信号量减到0了嘛!申请资源申请不到,无法消费,就在这里阻塞;生产者只有生产完,V操作之后,数据资源才会有数据。当_data_sem.P(); 值为0的时候,不就相当于锁本身先被别人抢走了,锁变为了0,此时就被锁住,所以,互斥特性就在这里体现出来。

4.2 version2 -- 多生产,多消费

多生产者,多消费者,就必须得要新增维护生产者之间,消费者之间的互斥关系啦!

多个生产者同时生产,有可能会对同一个下标进行生产,下标只有一个,下标本身又成了临界资源,所以得加锁,所以需要几把锁??怎么加??

生产者和消费者之间的同步互斥关系已经由信号量自动维护了,生产者和生产者之间,消费者和消费者之间也是互斥的,所以得加锁,今天的消费者和生产者除了为空,除了为满,其它的情况都是生产和消费在并发的跑的,选用一把锁的话,就放弃了生产和消费的并发的情况。没错,但是会降低效率。所以选用两把锁,生产者之间共用一把锁,消费者之间共用一把锁。

生产者先内部竞争,选出一个生产者,同样的消费者之间也内部竞争,选出来一个消费者,选用两把锁,本质上还是单生产单消费。

cpp 复制代码
#pragma once

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

static int gcap = 5;

template <typename T>
class RingQueue
{
public:
    RingQueue(int cap = gcap)
        : _cap(cap), _ring_queue(cap), _space_sem(cap), _data_sem(0), _p_step(0), _c_step(0)
    {
    }
    void Pop(T *out)
    {
        // 先申请数据资源
        _data_sem.P();
        // 走到下面,要么是为满的情况,要么是不为空不为满的情况,不用担心生产者影响到自己
        {   
            LockGuard lockguard(&_c_lock);
            *out = _ring_queue[_c_step++];
            _c_step %= _cap;
        }
        _space_sem.V();
    }
    void Enqueue(const T &in)
    {
        // 1. 先申请空间资源,再加锁
        _space_sem.P();
        {
            LockGuard lockguard(&_p_lock);
            // 生产数据,有空间,在哪里呀?
            _ring_queue[_p_step++] = in;
            // 单生产单消费中,生产者生产的时候,消费者不能来进行打扰,因为,进入到生产中,一定不为满,所有就只有为空或者是不为空不为满的情况
            // 不为空不为满二者可以并发执行,消费者并不影响生产者,为空的时候,生产者在运行期间,消费者的数据计数器为0,消费者不可能进入,就不会影响
            // 所以单生产单消费中,申请到了信号量我在放数据的时候没有人能影响到我
            // 维持环形特点
            _p_step %= _cap;
        }
        _data_sem.V();
    }
    ~RingQueue()
    {
    }

private:
    std::vector<T> _ring_queue; // 临界资源,环形队列的底层逻辑
    int _cap;                   // 总容量大小,进行模运算

    Sem _space_sem; // 空间资源,空间信号量 - 生产者关心
    Sem _data_sem;  // 数据资源,信号量是计数器,数据资源的数量

    // 生产和消费的位置
    int _p_step;
    int _c_step;

    // 定义两把锁
    Mutex _p_lock;
    Mutex _c_lock;
};

运行结果:

打出来的结果有重复的,这是正常的,因为生产数据时多个线程在生产的时候,不是生产的时候数据拿重复了,也不是消费的时候数据拿重复了,是因为多个线程识别data的时候,enqueue的时候被同时访问了,data的本身的代码没有做保护,但是这个不影响。

资源整体使用,选用互斥锁,资源允许局部使用,选用信号量。

互斥锁和信号量本质就是一个变量,变量本质就是一段空间,所以想在多进程中使用也是可以使用的。

相关推荐
Dr_eamboat3 小时前
SpringBoot策略模式+工厂模式实战解析
linux·spring boot·策略模式
wuminyu3 小时前
Java锁机制之轻量级锁判断与尝试逻辑源码剖析
java·linux·c语言·jvm·c++
阳光满路5 小时前
三步搞定:Linux 安装配置 Telnet 服务
linux·运维·centos
码农编程录5 小时前
【notes9】
linux
RisunJan6 小时前
Linux命令-objdump(显示二进制文件信息)
linux·运维
bloglin999997 小时前
TabClaw(交互式表格分析 AI 智能体)在线下载,离线部署
linux·运维·服务器·tabclaw
云栖梦泽7 小时前
WIFI通信测试
linux·运维·服务器·压力测试
Dlrb12117 小时前
Linux系统编程-进程回收
linux·exec·进程·进程回收