【Linux】信号量和线程池

欢迎来到Cefler的博客😁

🕌博客主页:折纸花满衣

🏠个人专栏:题目解析

🌎推荐文章:【Linux】进程通信------共享内存+消息队列+信号量


目录

👉🏻信号量

【Linux】进程通信------共享内存+消息队列+信号量中我们已经初步了解过了信号量。

信号量,一种用于进程间通信和同步的机制,主要用来控制对共享资源的访问。通过信号量,可以确保多个进程之间能够有序地访问共享资源,避免数据竞争等问题。

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

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

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

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

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

👉🏻POSIX信号量函数

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

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

sem_init

函数原型:

c 复制代码
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数意义:

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

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

使用代码示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>

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;
}

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

sem_wait

函数原型:

c 复制代码
int sem_wait(sem_t *sem);

参数意义:

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

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

使用代码示例:

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>

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;
}

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

sem_post and sem_destroy

sem_post 函数介绍:
函数原型:

c 复制代码
int sem_post(sem_t *sem);

参数意义:

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

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

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


sem_destroy 函数介绍:

函数原型:

c 复制代码
int sem_destroy(sem_t *sem);

参数意义:

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

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

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

环形队列


这里我们的规则是:

1.生产者不能把消费者套一个圈

2.消费者也不能超过生产者

只有为空和为满两种情况二者会指向同一位置,其它情况都是异步操作。

为空时只能让生产者跑,为满时只能让消费者跑。

RingQueue.hpp

cpp 复制代码
#pragma once

#include <iostream>
#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)
        : _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;
};
  • P 操作(等待信号量):如果信号量的计数器大于零,则将计数器减一,进程继续执行;否则,进程进入等待状态。
  • V 操作(释放信号量):将信号量的计数器加一,唤醒等待该信号量的其他进程。

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

LockGuard.hpp

cpp 复制代码
#pragma once

#include <pthread.h>

// 不定义锁,默认认为外部会给我们传入锁对象
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;
};

👉🏻线程池

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

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

  1. 工作原理:

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

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

常见线程池的组成部分:

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

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

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

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

总结:

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

小故事理解线程池

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

线程池的比喻:

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

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

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

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

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


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

相关推荐
cominglately3 小时前
centos单机部署seata
linux·运维·centos
魏 无羡3 小时前
linux CentOS系统上卸载docker
linux·kubernetes·centos
CircleMouse3 小时前
Centos7, 使用yum工具,出现 Could not resolve host: mirrorlist.centos.org
linux·运维·服务器·centos
木子Linux4 小时前
【Linux打怪升级记 | 问题01】安装Linux系统忘记设置时区怎么办?3个方法教你回到东八区
linux·运维·服务器·centos·云计算
mit6.8244 小时前
Ubuntu 系统下性能剖析工具: perf
linux·运维·ubuntu
鹏大师运维4 小时前
聊聊开源的虚拟化平台--PVE
linux·开源·虚拟化·虚拟机·pve·存储·nfs
watermelonoops4 小时前
Windows安装Ubuntu,Deepin三系统启动问题(XXX has invalid signature 您需要先加载内核)
linux·运维·ubuntu·deepin
滴水之功5 小时前
VMware OpenWrt怎么桥接模式联网
linux·openwrt
ldinvicible5 小时前
How to run Flutter on an Embedded Device
linux
YRr YRr6 小时前
解决Ubuntu 20.04上编译OpenCV 3.2时遇到的stdlib.h缺失错误
linux·opencv·ubuntu