Linux--信号量

目录

1.概念

 2.认识接口

 3.理论加代码

3.1问题背景

3.2解决方案

3.3代码实现


1.概念

信号量是什么?

想象一下你有一个小小的计数器,这个计数器不是用来数人数或者物品数量的,而是用来控制“访问权”的。这个特殊的计数器,我们就叫它“信号量”。

信号量的作用是什么?

信号量的主要作用是帮助多个“人”(我们可以把这里的“人”想象成进程、线程或者任何需要同步的实体)在争夺同一个“资源”(比如打印机、数据库连接、共享内存等)时保持秩序,避免混乱。

        之前我们知道被多个执行流同时访问的公共资源叫做临界资源,而临界资源不保护的话会造成数据不一性的问题。
        们用互斥锁保护临界资源是把这个临界资源当做一个整体,只能让1个执行流访问临界资源。现在我们把临界资源分割成多个区域,当多个执行流访问不同的区域,此时不会出现数据不一性的问题了。

每个执行流先申请信号量,申请到信号量后同时访问临界资源,访问完后释放信号量。

信号量怎么工作?

信号量有两个关键操作,我们称之为“P操作”和“V操作”。

  • P操作(等待/申请):当你想要使用一个资源时,你会走到信号量面前,告诉它你想要访问那个资源。信号量会看看它的小计数器上写着多少。如果计数器大于0,说明还有资源可用,它就会把计数器减1,然后让你进去使用资源。如果计数器是0,说明资源都被别人占用了,它会让你在一边等着,直到有人释放资源。

  • V操作(释放/通知):当你用完资源后,你会再次走到信号量面前,告诉它你已经用完了。信号量就会把它的计数器加1,表示又多了一个资源。然后,如果有人在等这个资源(因为之前的P操作被挡住了),信号量就会叫醒其中一个人,让他进去使用资源。


 2.认识接口

sem_init 函数是用于初始化一个未命名的信号量(semaphore)的函数。是线程间或进程间同步的一种机制。通过信号量,程序可以控制对共享资源的访问,确保在同一时间内只有一个线程(或进程)能够访问某个特定的资源。

参数

  • sem:指向要初始化的信号量对象的指针。
  • pshared:控制信号量的作用域。如果pshared的值非0,则信号量在进程间共享;如果为0,则信号量仅在调用进程内的线程间共享。
  • value:信号量的初始值。这个值必须是非负的。

返回值

  • 成功时,sem_init 返回0。
  • 失败时,返回-1,并设置errno以指示错误

sem_destroy函数的作用是释放与信号量相关联的资源,确保系统资源的正确回收。

参数

  • sem:指向要销毁的信号量对象的指针。

返回值

  • 成功时,sem_destroy 返回 0。
  • 失败时,返回 -1,并设置 errno 以指示错误。可能的错误包括 EINVAL,表示传入的信号量不是一个有效的信号量。

sem_wait函数是信号量同步机制中的关键部分,用于实现线程或进程间的同步。

该函数用于信号量的P操作,从信号量的值中减去1,如果信号量的值变为0,则当前线程将被阻塞,直到信号量的值被其他线程通过 sem_post 增加。

参数

  • sem:指向要操作的信号量对象的指针。

返回值

  • 成功时返回0。
  • 失败时返回-1,并设置errno以指示错误。

sem_post函数用于信号量的V操作,主要作用是给信号量的值加上一个“1”,并可能唤醒一个或多个在该信号量上等待的线程。

参数

  • sem:指向要操作的信号量的指针。

功能

  • 增加信号量值sem_post函数将信号量的值增加1。
  • 唤醒等待线程:如果有线程因为调用sem_wait函数而在该信号量上等待(即信号量的值为0且线程被阻塞),那么sem_post函数可能会唤醒其中一个等待的线程。具体唤醒哪个线程取决于操作系统的线程调度策略。

返回值

  • 成功时,sem_post返回0。
  • 失败时,返回-1,并设置errno以指示错误。可能的错误包括EINVAL,表示传入的信号量不是一个有效的信号量。


 3.理论加代码

3.1问题背景

如何基于环形队列实现生产消费模型:

        环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留一个空的位置,作为满的状态
 

比如消费者(head)指向头,生产者(end)指向尾。

当head和end 指向一个位置时:

        1.队列为空,让谁先访问?当然只能让生产者先生产。

        2.队列满了,让谁再访问?当然只能让消费者来消费

        这其实就满足了一定顺序性了(同步),而且是满足了互斥特定的

head和end指向不同的位置:

        队列不为空&&队列不为满--此时生产和消费同时进行!因为head和end方位的是同一块资源的不同位置,多线程当然可以并发访问的了。

        基于以上两种情况,我们就可以让线程再局部上体现互斥和同步的特点,在宏观上体现出并发了。

结论:

        a.不让生产者把消费者套一个圈。

        b.不能让消费者,超过生产者

以上条件,用信号量(用于做互斥与同步的)就能满足了。        


3.2解决方案

        对于以上问题,消费者最关心的是数据资源(取数据)。对于生产者来说,最关心的就是空间资源(放数据)。信号量就是描述资源数量的。因此我们可以定义两个信号量:

sem_t data_sem=0,初始化肯定是0(数据资源),sem_t space_sem=N(空间资源)。

  •         生产者p(space_sem)申请空间资源--,生产动作v(data_sem)数据资源++,生产者申请到空间了,但数据还是在那块空间,当然v的是数据的信号量。
  •         消费者p(data_sem)申请数据资源--,消费动作v(space_sem)空间资源++,消费者把数据拿走了只留下空间了,当然v的是空间的信号量。

        一开始,data资源为0,也就是为空情况,消费线程和生产线程都来了,消费线程是注定要被挂起。deat资源为N,也就是为满的情况,消费线程和生产线程都来了,生产线程主动要被挂起。这就完成了互斥(理论上互斥)

        data和space达到动态平衡时,消费线程和生产线程都能进来,他们的两个下标也不会指向同一个位置,这就达到了并发的效果。

        因为data和space资源最大为N,这注定了生产线程不能一直无脑生产最大为N,消费者不能无脑消费,最大为N。


3.3代码实现

 RingQueue.hpp

#pragma once

#include <iostream>
#include <vector>
#include <string>
#include <pthread.h>
#include <semaphore.h>

template <typename T>
class RingQueue
{
private:
    void P(sem_t &s)
    {
        sem_wait(&s);//对信号量--
    }
    void V(sem_t &s)
    {
        sem_post(&s);//对信号量++
    }
public:
    RingQueue(int max_cap)
        : _ringqueue(max_cap), _max_cap(max_cap), _c_step(0), _p_step(0)
    {
        sem_init(&_data_sem, 0, 0);
        sem_init(&_space_sem, 0, max_cap);

        pthread_mutex_init(&_c_mutex, nullptr);
        pthread_mutex_init(&_p_mutex, nullptr);
    }
    void Push(const T &in) //生产者
    {
        
        P(_space_sem); 
        pthread_mutex_lock(&_p_mutex); //?
        _ringqueue[_p_step] = in;//生产的位置
        _p_step++;
        _p_step %= _max_cap;//维持环状
        pthread_mutex_unlock(&_p_mutex);
        V(_data_sem);//数据资源多了一个
    }
    void Pop(T *out) // 消费
    {
        P(_data_sem);//申请数据资源
        pthread_mutex_lock(&_c_mutex); //?
        *out = _ringqueue[_c_step];//在消费者下标进行消费
        _c_step++;
        _c_step %= _max_cap;
        pthread_mutex_unlock(&_c_mutex);
        V(_space_sem);//数据取走,空间空出来,归还空间
    }
    ~RingQueue()
    {
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);

        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }
private:
    std::vector<T> _ringqueue;//vector模拟的环形队列结构
    int _max_cap;//容量

    int _c_step;//生产者下标
    int _p_step;//消费者下标

    sem_t _data_sem; // 消费者关心
    sem_t _space_sem; // 生产者关心

    pthread_mutex_t _c_mutex;//生产者互斥锁
    pthread_mutex_t _p_mutex;//消费者互斥锁
};

细节:

        1.为了完成多生产和多消费的模型,消费者和生产者的同步和互斥问题我们的消费队列已经解决了。那么我们就要维护生产者和生产者的互斥关系,还有消费者和消费者之间的互斥关系。由于环形队列的下标也是属于临界资源的,如果不维持关系内部的互斥关系,是一定会破坏环形队列结构的。所以势必要引入生产者互斥锁和消费者互斥锁。

        2.先加锁好还是先申请获取信号量好呢?

        先申请信号量好,所有线程先瓜分好信号量,在其它线程进行等待锁的时候,此时资源已经申请好了,这样提高了解决问题的实际效率。

         3.信号量这里,对资源进行使用,申请,为什么不判断一下条件是否满足?

          信号量本身就是判断条件! 信号量:是一个计数器,是资源的预订机制。预订:在外部,可以不判断资源是否满足,就可以知道内部资源的情况!

        信号量原理:一元信号量(或二元信号量)主要用于实现资源的互斥访问。当一个线程(或进程)获取了信号量(将其值从1减为0),它便获得了对某个共享资源的独占访问权,其他试图获取该信号量的线程将被阻塞,直到信号量被释放(其值从0加回1)。

        信号量对公共资源使用时可以整体使用,也可以不整体使用。整体使用就是把整个资源看作一份。当看整体的时候,当二元信号量的值为1时,表示资源未被占用,线程可以获取信号量并进入临界区;当信号量的值为0时,表示资源已被占用,其他线程必须等待。那么这就等同于互斥锁了。

Task.hpp:

#pragma once

#include<iostream>


class Task
{
public:
    Task()
    {
    }
    Task(int x, int y) : _x(x), _y(y)
    {
    }
    void Excute()
    {
        _result = _x + _y;
    }
    void operator ()()
    {
        Excute();
    }
    std::string debug()
    {
        std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=?";
        return msg;
    }
    std::string result()
    {
        std::string msg = std::to_string(_x) + "+" + std::to_string(_y) + "=" + std::to_string(_result);
        return msg;
    }

private:
    int _x;
    int _y;
    int _result;
};

main.cc:多生产者生产,多消费者消费

#include "RingQueue.hpp"
#include "Task.hpp"
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>

void *Consumer(void*args)
{
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);
    while(true)
    {
        Task t;
        // 1. 消费
        rq->Pop(&t);

        // 2. 处理数据
        t();
        std::cout << "Consumer-> " << t.result() << std::endl;
    }
}
void *Productor(void*args)
{
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);

    while(true)
    {
        sleep(1);

        // 1. 构造数据
        int x = rand() % 10 + 1; //[1, 10]
        usleep(x*1000);
        int y = rand() % 10 + 1;
        Task t(x, y);

        // 2. 生产
        rq->Push(t);

        std::cout << "Productor -> " << t.debug() << std::endl;
    }
}

int main()
{
    srand(time(nullptr) ^ getpid());
    RingQueue<Task> *rq = new RingQueue<Task>(5);
    // 单单
    pthread_t c1, c2, p1, p2, p3;
    pthread_create(&c1, nullptr, Consumer, rq);
    pthread_create(&c2, nullptr, Consumer, rq);
    pthread_create(&p1, nullptr, Productor, rq);
    pthread_create(&p2, nullptr, Productor, rq);
    pthread_create(&p3, nullptr, Productor, rq);


    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    pthread_join(p3, nullptr);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值