Linux--信号量

07-19 819阅读

目录

1.概念

 2.认识接口

 3.理论加代码

3.1问题背景

3.2解决方案

3.3代码实现


1.概念

信号量是什么?

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

信号量的作用是什么?

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

        之前我们知道被多个执行流同时访问的公共资源叫做临界资源,而临界资源不保护的话会造成数据不一性的问题。

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

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

Linux--信号量

信号量怎么工作?

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

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

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


     2.认识接口

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

    Linux--信号量

    参数

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

      返回值

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

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

        Linux--信号量

        参数

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

          返回值

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

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

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

            Linux--信号量

            参数

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

              返回值

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

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

                Linux--信号量

                参数

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

                  功能

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

                    返回值

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

                       3.理论加代码

                      3.1问题背景

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

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

                       

                      Linux--信号量

                      比如消费者(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 
                        #include 
                        #include 
                        #include 
                        #include 
                        template 
                        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 _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时,表示资源已被占用,其他线程必须等待。那么这就等同于互斥锁了。

                        Linux--信号量

                        Task.hpp:

                        #pragma once
                        #include
                        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 
                        #include 
                        #include 
                        #include 
                        void *Consumer(void*args)
                        {
                            RingQueue *rq = static_cast(args);
                            while(true)
                            {
                                Task t;
                                // 1. 消费
                                rq->Pop(&t);
                                // 2. 处理数据
                                t();
                                std::cout 
VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]