欢迎来到Cefler的博客😁
🕌博客主页:折纸花满衣
🏠个人专栏:题目解析
🌎推荐文章:【Linux】进程通信——共享内存+消息队列+信号量
目录
- 👉🏻信号量
- 👉🏻POSIX信号量函数
- sem_init
- sem_wait
- sem_post and sem_destroy
- 👉🏻基于环形队列的生产者消费者模型使用信号量实现线程同步
- 环形队列
- RingQueue.hpp
- LockGuard.hpp
- 👉🏻线程池
- 小故事理解线程池
👉🏻信号量
在【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);
参数意义:
sem
:指向要初始化的信号量的指针。pshared
:指定信号量的类型。如果为0,表示信号量是进程内共享的;如果非0,表示信号量可以在进程间共享(需要使用命名信号量)。value
:指定信号量的初始值。
函数功能:该函数用于初始化一个信号量。
使用代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>int main() {sem_t mySemaphore;// 初始化一个进程内共享的信号量,初始值为1if (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
函数原型:
int sem_wait(sem_t *sem);
参数意义:
sem
:指向要操作的信号量的指针。
函数功能:该函数用于对信号量进行等待操作。如果信号量的值大于0,表示可以继续执行;如果信号量的值为0,则调用该函数的线程会被阻塞,直到信号量的值变为非零。
使用代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>int main() {sem_t mySemaphore;// 初始化一个进程内共享的信号量,初始值为1if (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 函数介绍:
函数原型:
int sem_post(sem_t *sem);
参数意义:
sem
:指向要操作的信号量的指针。
函数功能:该函数用于对信号量进行释放操作。它会将信号量的值加1,并唤醒等待该信号量的线程(如果有的话)。
发布信号量,表示资源使用完毕,可以归还资源了
sem_destroy 函数介绍:
函数原型:
int sem_destroy(sem_t *sem);
参数意义:
sem
:指向要销毁的信号量的指针。
函数功能:该函数用于销毁一个信号量,并释放其占用的资源。调用 sem_destroy
后,该信号量就不能再被使用,需要重新初始化才能再次使用。
👉🏻基于环形队列的生产者消费者模型使用信号量实现线程同步
环形队列
这里我们的规则是:
1.生产者不能把消费者套一个圈
2.消费者也不能超过生产者
只有为空和为满两种情况二者会指向同一位置,其它情况都是异步操作。
为空时只能让生产者跑,为满时只能让消费者跑。
RingQueue.hpp
#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,还是先申请信号量?2P(_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
#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)是一种多线程处理任务的机制,它包含一个线程集合,这些线程在后台等待任务,并在任务到来时执行任务。线程池通常用于提高多线程应用程序的性能和效率,避免频繁创建和销毁线程所带来的开销。
线程池的工作原理和优势:
-
工作原理:
- 线程池由一组预先创建好的线程组成,这些线程在初始化时被启动并保持运行状态。
- 当有任务到达时,线程池会从空闲线程中选择一个线程来执行任务,而不是每次都创建新线程。
- 执行完任务后,线程不会销毁,而是继续保持在线程池中,可以继续执行其他任务。
-
优势:
- 降低线程创建和销毁的开销:线程池中的线程可以重复利用,减少了线程创建和销毁的开销。
- 控制线程数量:线程池可以限制同时运行的线程数量,避免线程过多导致系统资源耗尽。
- 提高响应速度:线程池中的线程在任务到达时立即执行,不需要等待线程创建,提高了任务的响应速度。
- 资源管理:通过线程池可以更好地管理资源,控制并发度,避免系统负载过重。
常见线程池的组成部分:
-
工作队列(Task Queue): 用于存放待执行的任务,线程池中的空闲线程会从队列中取出任务执行。
-
线程管理模块: 负责线程的创建、销毁和分配任务给线程执行。
-
线程池管理模块: 负责线程池的初始化、销毁,以及控制线程数量等参数的设置。
-
线程: 线程池中实际执行任务的工作单元。
总结:
线程池是一种用于管理多线程任务执行的机制,通过预先创建一组线程并维护一个任务队列,可以有效提高多线程应用程序的性能和效率,减少资源消耗和提高系统响应速度。在实际应用中,线程池被广泛应用于各种需要并发处理任务的场景,如网络服务器、数据库连接池等。
小故事理解线程池
假设你是一家餐厅的老板,经常会有顾客来吃饭。为了提高效率,你决定雇佣一些服务员来为顾客提供服务。
线程池的比喻:
你创建了一个名为"服务员线程池"的团队,该团队由多个服务员组成。他们在餐厅的大厅里等待顾客的到来。
-
工作队列(Task Queue): 这个队列就像是餐厅门口的候位区,顾客到来后会排队等待就餐。每个顾客就是一个任务,需要被执行。
-
线程管理模块: 你作为老板负责管理这些服务员。当有顾客(任务)到来时,你从候位区(工作队列)中选择一个空闲的服务员(线程)来接待顾客,并将任务分配给他。
-
线程池管理模块: 你根据餐厅的需求决定雇佣多少个服务员(线程),并设置最大的服务员数量。如果餐厅太忙,所有的服务员都在忙碌,新到来的顾客需要等待。但是如果餐厅没有太多的顾客,有些服务员就会闲置在那里。
-
线程: 每个服务员都是一个线程,他们在餐厅中接待顾客(执行任务)。一旦任务完成,服务员(线程)不会被销毁,而是回到等待区(线程池),继续等待下一个顾客(任务)的到来。
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长