• 【Linux】信号量和线程池


    在这里插入图片描述

    欢迎来到Cefler的博客😁
    🕌博客主页:折纸花满衣
    🏠个人专栏:题目解析
    🌎推荐文章:【Linux】进程通信——共享内存+消息队列+信号量

    在这里插入图片描述


    👉🏻信号量

    【Linux】进程通信——共享内存+消息队列+信号量中我们已经初步了解过了信号量。
    信号量,一种用于进程间通信和同步的机制,主要用来控制对共享资源的访问。通过信号量,可以确保多个进程之间能够有序地访问共享资源,避免数据竞争等问题。

    现在,让我们来举一个幽默风趣的例子来帮助理解信号量的概念:

    假设有一个办公室里只有一个咖啡机,而办公室里有三个员工:小明、小红和小李。每个员工都爱喝咖啡,但是咖啡机一次只能供应一个人使用。

    这时,我们可以用一个信号量来模拟这个场景。信号量的初始值为1,代表咖啡机可供使用。当一个员工想要喝咖啡时,他会尝试获取信号量,如果信号量的值大于0(咖啡机可供使用),他就可以使用咖啡机,然后将信号量减1。当他喝完咖啡后,会释放信号量,让其他员工可以使用咖啡机。

    如果此时另外两个员工也想要喝咖啡,由于信号量的值已经为0,他们会等待直到有人释放信号量为止。这样就避免了多个员工同时使用咖啡机,保证了咖啡机的有序使用。

    信号量本质就是一个资源的计数器

    👉🏻POSIX信号量函数

    【Linux】进程通信——共享内存+消息队列+信号量中我们已经学过了SystemV信号量函数的使用,而在这篇文章里,我们将学习POSIX信号量函数进行实现线程间的同步。

    POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步

    sem_init

    函数原型:

    int sem_init(sem_t *sem, int pshared, unsigned int value);
    
    • 1

    参数意义:

    • sem:指向要初始化的信号量的指针。
    • pshared:指定信号量的类型。如果为0,表示信号量是进程内共享的;如果非0,表示信号量可以在进程间共享(需要使用命名信号量)。
    • value:指定信号量的初始值。

    函数功能:该函数用于初始化一个信号量。

    使用代码示例:

    #include 
    #include 
    #include 
    
    int main() {
        sem_t mySemaphore;
        
        // 初始化一个进程内共享的信号量,初始值为1
        if (sem_init(&mySemaphore, 0, 1) == -1) {
            perror("Semaphore initialization failed");
            exit(EXIT_FAILURE);
        }
    
        // 在这里可以使用信号量进行同步操作
        // ...
    
        // 销毁信号量
        sem_destroy(&mySemaphore);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这个示例中,我们使用sem_init函数初始化了一个进程内共享的信号量mySemaphore,初始值为1。接下来可以在代码中使用这个信号量进行同步操作。最后,在程序结束前使用sem_destroy函数销毁信号量。

    sem_wait

    函数原型:

    int sem_wait(sem_t *sem);
    
    • 1

    参数意义:

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

    函数功能:该函数用于对信号量进行等待操作。如果信号量的值大于0,表示可以继续执行;如果信号量的值为0,则调用该函数的线程会被阻塞,直到信号量的值变为非零。

    使用代码示例:

    #include 
    #include 
    #include 
    
    int main() {
        sem_t mySemaphore;
        
        // 初始化一个进程内共享的信号量,初始值为1
        if (sem_init(&mySemaphore, 0, 1) == -1) {
            perror("Semaphore initialization failed");
            exit(EXIT_FAILURE);
        }
    
        // 等待信号量的值变为非零
        if (sem_wait(&mySemaphore) == -1) {
            perror("Semaphore wait failed");
            exit(EXIT_FAILURE);
        }
    
        // 在这里可以执行需要同步的操作
        // ...
    
        // 释放信号量
        if (sem_post(&mySemaphore) == -1) {
            perror("Semaphore post failed");
            exit(EXIT_FAILURE);
        }
    
        // 销毁信号量
        sem_destroy(&mySemaphore);
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    在这个示例中,我们使用sem_init函数初始化了一个进程内共享的信号量mySemaphore,初始值为1。接下来,调用sem_wait函数等待信号量的值变为非零。如果信号量的值为0,调用线程会被阻塞,直到信号量的值变为非零。在需要同步的操作完成后,我们使用sem_post函数释放信号量。最后,在程序结束前使用sem_destroy函数销毁信号量。

    sem_post and sem_destroy

    sem_post 函数介绍:
    函数原型:

    int sem_post(sem_t *sem);
    
    • 1

    参数意义:

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

    函数功能:该函数用于对信号量进行释放操作。它会将信号量的值加1,并唤醒等待该信号量的线程(如果有的话)。

    发布信号量,表示资源使用完毕,可以归还资源了


    sem_destroy 函数介绍:

    函数原型:

    int sem_destroy(sem_t *sem);
    
    • 1

    参数意义:

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

    函数功能:该函数用于销毁一个信号量,并释放其占用的资源。调用 sem_destroy 后,该信号量就不能再被使用,需要重新初始化才能再次使用。

    👉🏻基于环形队列的生产者消费者模型使用信号量实现线程同步

    环形队列

    在这里插入图片描述
    这里我们的规则是:
    1.生产者不能把消费者套一个圈
    2.消费者也不能超过生产者

    只有为空和为满两种情况二者会指向同一位置,其它情况都是异步操作。
    为空时只能让生产者跑,为满时只能让消费者跑。

    RingQueue.hpp

    #pragma once
    
    #include 
    #include 
    #include 
    #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)
            : _ringqueue(size), _size(size), _p_step(0), _c_step(0)
        {
            sem_init(&_space_sem, 0, size);
            sem_init(&_data_sem, 0, 0);
    
            pthread_mutex_init(&_p_mutex, nullptr);
            pthread_mutex_init(&_c_mutex, nullptr);
        }
        void Push(const T &in)
        {
            // 生产
            // 先加锁1,还是先申请信号量?2
            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()
        {
            sem_destroy(&_space_sem);
            sem_destroy(&_data_sem);
    
            pthread_mutex_destroy(&_p_mutex);
            pthread_mutex_destroy(&_c_mutex);
        }
    
    private:
        std::vector<T> _ringqueue;
        int _size;
    
        int _p_step; // 生产者的生产位置
        int _c_step; // 消费位置
    
        sem_t _space_sem; // 生产者的信号量(计数器)
        sem_t _data_sem;  // 消费者的信号量(计数器)
    
        pthread_mutex_t _p_mutex;
        pthread_mutex_t _c_mutex;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • P 操作(等待信号量):如果信号量的计数器大于零,则将计数器减一,进程继续执行;否则,进程进入等待状态。
    • V 操作(释放信号量):将信号量的计数器加一,唤醒等待该信号量的其他进程。

    总而言之在这里,P操作就是进行资源1——>资源2的利用转换(上述代码是空间和数据的转换)并对旧资源信号量的计数器减1,V操作就是对新增资源信号量的计数器加1

    LockGuard.hpp

    #pragma once
    
    #include 
    
    // 不定义锁,默认认为外部会给我们传入锁对象
    class Mutex
    {
    public:
        Mutex(pthread_mutex_t *lock):_lock(lock)
        {}
        void Lock()
        {
            pthread_mutex_lock(_lock);
        }
        void Unlock()
        {
            pthread_mutex_unlock(_lock);
        }
        ~Mutex()
        {}
    
    private:
        pthread_mutex_t *_lock;
    };
    
    class LockGuard
    {
    public:
        LockGuard(pthread_mutex_t *lock): _mutex(lock)
        {
            _mutex.Lock();
        }
        ~LockGuard()
        {
            _mutex.Unlock();
        }
    private:
        Mutex _mutex;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    👉🏻线程池

    在这里插入图片描述

    线程池(Thread Pool)是一种多线程处理任务的机制,它包含一个线程集合,这些线程在后台等待任务,并在任务到来时执行任务。线程池通常用于提高多线程应用程序的性能和效率,避免频繁创建和销毁线程所带来的开销。

    线程池的工作原理和优势:

    1. 工作原理:

      • 线程池由一组预先创建好的线程组成,这些线程在初始化时被启动并保持运行状态。
      • 当有任务到达时,线程池会从空闲线程中选择一个线程来执行任务,而不是每次都创建新线程。
      • 执行完任务后,线程不会销毁,而是继续保持在线程池中,可以继续执行其他任务。
    2. 优势:

      • 降低线程创建和销毁的开销:线程池中的线程可以重复利用,减少了线程创建和销毁的开销。
      • 控制线程数量:线程池可以限制同时运行的线程数量,避免线程过多导致系统资源耗尽。
      • 提高响应速度:线程池中的线程在任务到达时立即执行,不需要等待线程创建,提高了任务的响应速度。
      • 资源管理:通过线程池可以更好地管理资源,控制并发度,避免系统负载过重。

    常见线程池的组成部分:

    1. 工作队列(Task Queue): 用于存放待执行的任务,线程池中的空闲线程会从队列中取出任务执行。

    2. 线程管理模块: 负责线程的创建、销毁和分配任务给线程执行。

    3. 线程池管理模块: 负责线程池的初始化、销毁,以及控制线程数量等参数的设置。

    4. 线程: 线程池中实际执行任务的工作单元。

    总结:
    线程池是一种用于管理多线程任务执行的机制,通过预先创建一组线程并维护一个任务队列,可以有效提高多线程应用程序的性能和效率,减少资源消耗和提高系统响应速度。在实际应用中,线程池被广泛应用于各种需要并发处理任务的场景,如网络服务器、数据库连接池等。

    小故事理解线程池

    假设你是一家餐厅的老板,经常会有顾客来吃饭。为了提高效率,你决定雇佣一些服务员来为顾客提供服务。
    在这里插入图片描述

    线程池的比喻:

    你创建了一个名为"服务员线程池"的团队,该团队由多个服务员组成。他们在餐厅的大厅里等待顾客的到来。

    1. 工作队列(Task Queue): 这个队列就像是餐厅门口的候位区,顾客到来后会排队等待就餐。每个顾客就是一个任务,需要被执行。

    2. 线程管理模块: 你作为老板负责管理这些服务员。当有顾客(任务)到来时,你从候位区(工作队列)中选择一个空闲的服务员(线程)来接待顾客,并将任务分配给他。

    3. 线程池管理模块: 你根据餐厅的需求决定雇佣多少个服务员(线程),并设置最大的服务员数量。如果餐厅太忙,所有的服务员都在忙碌,新到来的顾客需要等待。但是如果餐厅没有太多的顾客,有些服务员就会闲置在那里。

    4. 线程: 每个服务员都是一个线程,他们在餐厅中接待顾客(执行任务)。一旦任务完成,服务员(线程)不会被销毁,而是回到等待区(线程池),继续等待下一个顾客(任务)的到来。


    如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长
    在这里插入图片描述
    在这里插入图片描述

  • 相关阅读:
    内核进程的调度与进程切换
    安装ipfs-swarm-key-gen
    cocos creator 在网页中调试的时候直接代码调试方法
    BGP学习笔记
    伤停等待(wound-wait)在分布式事务中
    C/C++读写二进制文件
    CTF是黑客大赛?新手如何入门CTF?
    阿里云服务器计算型、通用型、内存型各实例计算、存储等性能介绍
    Node.js基础---Express
    mongodb 5.0.14 副本集 安装 单机版 记录
  • 原文地址:https://blog.csdn.net/cefler/article/details/136768341