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


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

相关推荐
vvw&37 分钟前
如何在 Ubuntu 上安装 Jupyter Notebook
linux·人工智能·python·opencv·ubuntu·机器学习·jupyter
钰爱&4 小时前
【操作系统】Linux之线程同步二(头歌作业)
linux·运维·算法
Yz98766 小时前
Hive基础
大数据·linux·数据仓库·hive·hadoop·bigdata
Stara05117 小时前
Linux系统常用操作与命令指南
linux·vim
white.tie8 小时前
linux配置nginx
linux·运维·nginx
Komorebi.py8 小时前
【Linux】-学习笔记03
linux·笔记·学习
dessler9 小时前
云计算&虚拟化-kvm创建网桥(bridge)
linux·运维·云计算
YRr YRr9 小时前
Ubuntu20.04 解决一段时间后键盘卡死的问题 ubuntu
linux·数据库·ubuntu
醇氧10 小时前
ab (Apache Bench)的使用
linux·学习·centos·apache
moneyxjj11 小时前
Linux各种解压命令汇总
linux·运维·服务器