news 2026/9/3 8:27:23

Linux:信号量,生产者消费者模型和日志实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux:信号量,生产者消费者模型和日志实现

目录

1.什么是信号量

2.基于环形队列的生产者消费者模型

3.日志的实现


1.什么是信号量

信号量的本质是一把计数器,描述资源的数量。申请信号量的本质是用来预定资源的,比如买电影票。因为信号量也是共享资源,同时只能一个线程访问,所以访问信号量直接设计成了原子操作。

  1. 信号量的本质是一把计数器。
  2. 申请信号本质就是预定资源
  3. PV操作是原子的

多个线程可以同时访问临界资源的条件

  1. 如果公共资源当作整体
  2. 多个线程不访问临界资源的同一个位置

1.申请信号量,2.访问指定的一个位置,3.释放信号量,这样就可以多个进程并发地访问这份资源。只要申请进来之后就不用再判断有没有了,因为通过信号量已经确定了,肯定可以分配给你。

信号量可以定义全局和局部的,sem 就是要传入的信号量。value 就是有多少资源位置

#include <semaphore.h> 信号量 int sem_init(sem_t *sem, int pshared, unsigned int value); 参数: pshared:0表示线程间共享,非零表示进程间共享 value:信号量初始值

销毁信号量

int sem_destroy(sem_t *sem);

等待和发布信号量

功能:等待信号量,会将信号量的值减1 int sem_wait(sem_t *sem); //P() 功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。 int sem_post(sem_t *sem);//V()

2.基于环形队列的生产者消费者模型

上一篇生产者与消费者模型是基于queue的,整体使用是安全的,其空间可以动态分配,这里基于固定大小的环形队列重写这个程序

有两点需要注意

  1. 生产者不能把消费者套一个圈:
  2. 消费者不能超过生产者

生产者与消费者只有两个场景会指向同一个位置,需要同步:为空为满其他情况,他们根本就不会指向同一个位置,可以同时执行,不用加锁。空时只能让生产者生产,满时只能让消费之消费,局部需要维持互斥与同步

不同角色对资源的认识:生产者需要空间 sem_space ,消费者需要数据 sem_data 。假设有N个位置,开始时 sem_space = N ,sem_data = 0;

生产者伪代码: P(sem_space) //生产 V(sem_data) 消费者伪代码: P(sem_data) //消费 V(sem_space)

两者当发现没有资源时,都会被阻塞。

单生产单消费,RingQueue代码:

#pragma once #include <iostream> #include <vector> #include <semaphore.h> const int defaultsize = 5; template<class T> class RingQueue { 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) { //定义信号量 sem_init(&_space_sem,0,_size);//有_size个空间 sem_init(&_data_sem,0,0);//最开始没有数据,只有空间 } void Push(const T& in) { //生产 P(_space_sem);//申请到说明一定有空间 _ringqueue[_p_step] = in; _p_step++; _p_step %= _size; V(_data_sem); } void Pop(T* out) { P(_data_sem);//申请到说明一定有数据 *out = _ringqueue[_c_step]; _c_step++; _c_step %= _size; V(_space_sem); } ~RingQueue() { sem_destroy(&_space_sem); sem_destroy(&_data_sem); } private: std::vector<T> _ringqueue; int _size; int _p_step;//生产者的脚步 int _c_step;//消费者的脚步 sem_t _space_sem;//生产者关心的信号量,因为生产和消费有时可以并发进行,所有要两个。 sem_t _data_sem;//消费者关心的信号量 };

用到了两个信号量,_space_sem,_data_sem,生产者申请等待 _space_sem,消费者申请等待 _data_sem,因为信号量是公共资源,要互斥访问,所以申请获得信号量这些操作设计是原子的,释放也是原子的。获得信号量之后说明一定可以对资源进行操作。

V的两个作用是“释放资源 + 若有等待者则唤醒它”,所以单生产单消费会看到生产者生产后消费者会跟着执行的现象。

与阻塞队列的不同,阻塞队列是当作一个整体来使用的,所以需要整体加锁,这里是通过信号量局部性地访问。

多生产多消费中,这里生产者与消费者的互斥与同步(空满,通过信号量)已经实现了,但是生产者与生产者之间,消费者与消费者之间的互斥并没有实现,因为比如多个生产者得到信号量进入到队列内部,但是内部的_p_step 是共享的,所以我们需要加锁,防止生产完没有 _p_step++,导致数据被覆盖。

信号量通过“阻塞/唤醒”机制,没有加锁,强制了生产者和消费者在缓冲区空/满时进行“同步交接”。这种机制比加锁更轻量,因为加锁通常是“主动忙等”或“上下文切换”,而信号量直接管理了资源计数。当队列不为空满时,如果只定义一把锁,生产和消费就不可能一起跑了, 所以这里定义两把锁

在环形队列中,多生产者多消费者先申请信号量还是先申请锁都是可以的,但是先申请信号量更快

  • 先申请锁再申请信号量:当多个线程到来时,他们都会在锁外等待,有锁时才会再申请信号量,一个锁一个信号量地来。
  • 多个线程先申请信号量:等待锁时多个线程可以申请信号量,之后就只需要等待锁然后访问资源就行。

多生产多消费RingQueue:

#pragma once #include <iostream> #include <vector> #include <semaphore.h> #include <pthread.h> const int defaultsize = 5; template<class T> class RingQueue { 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) { //定义信号量 sem_init(&_space_sem,0,_size);//有_size个空间 sem_init(&_data_sem,0,0);//最开始没有数据,只有空间 pthread_mutex_init(&_p_mutex,nullptr); pthread_mutex_init(&_c_mutex,nullptr); } void Push(const T& in) { //生产 P(_space_sem);//申请到说明一定有空间 pthread_mutex_lock(&_p_mutex);//写在里面效率高 _ringqueue[_p_step] = in; _p_step++; _p_step %= _size; 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 %= _size; pthread_mutex_unlock(&_c_mutex); 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;//消费者的锁 };

3.日志的实现

可变参数列表可以设计在函数参数中的,但不能只有...,前面要有一个合法参数,与函数调用压栈有关。我们要使用 va_list args,就是char*; 保存可变参数列表,va_start(args,前面的参数),原理是压栈时参数在一起排列。然后 va_arg(args,类型) 就可以得到传入的该类型参数,类型就是如 printf("%d,%s", , );printf 会在前面字符串提取,va_end(args),指向nullptr,清理参数。

可变参数列表使用示例:计算可变参数列表传入数的和,第一个参数是数据的个数

#include <stdio.h> #include <stdarg.h> int sum(int count, ...) { va_list args; int sum = 0; va_start(args,count); for(int i = 0;i < count ;i++) { int value = va_arg(args,int); sum += value; } va_end(args); return sum; } int main() { int result = sum(3,1,3,5); printf("%d\n",result);//9 return 0; }

关于日志实现时间:

通过 time 我们可以获取当前的时间戳(time_t 类型,也就是uint64_t)。

#include <time.h> struct tm* localtime(const time_t *timep);

localtime 会返回一个结构体,有些时间是从0开始的,内容如下图所示:

一个简单的日志代码:

#include <iostream> #include <fstream> #include <stdarg.h> #include <cstdarg> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <sys/fcntl.h> enum{ Debug = 0, Info, Warning, Error, Fatal, }; //打印到哪里 enum { Screen = 10, OneFile, ClassFile }; std::string LevelToString(int level) { switch(level) { case Debug: return "Debug"; case Info: return "Info"; case Warning: return "Warning"; case Error: return "Error"; case Fatal: return "Fatal"; default: return "Unknow"; } } const int defultstyle = Screen; const std::string default_filename = "testlog."; const std::string logdir = "log"; class Log { public: Log():style(defultstyle),filename(default_filename) { mkdir(logdir.c_str(),0755);//存放日志的目录 }; void Enable(int sty)//打印的方式 { style = sty; } std::string TimeStampExLocalTime()//获取当前时间 { time_t currtime = time(nullptr); struct tm* curr = localtime(&currtime); char time_buffer[128]; //格式化输入到 time_buffer snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d",\ curr->tm_year+1900,curr->tm_mon+1,curr->tm_mday,curr->tm_hour,curr->tm_min,curr->tm_sec); return time_buffer; } void WriteLogToOneFile(const std::string &logname,const std::string &message) { //c方式 umask(0); int fd = open(logname.c_str(),O_CREAT|O_WRONLY|O_APPEND,0666); if(fd < 0) return; write(fd, message.c_str(),message.size()); close(fd); //c++方式 // std::ofstream out(logname,std::ofstream::app); // if(!out.is_open()) // return; // out.write(message.c_str(),message.size()); // //out<<message; // out.close(); } void WriteLogToClassFile(const std::string &levelstr,const std::string&message) { std::string logname = logdir;//往指定路径下打印 logname += "/"; logname += filename; logname += levelstr;//通过文件后缀区分文件 WriteLogToOneFile(logname,message); } void WriteLog(const std::string &levelstr,const std::string &message) { switch(style) { case Screen: std::cout << message; break; case OneFile: WriteLogToClassFile("all",message); break; case ClassFile: WriteLogToClassFile(levelstr,message); break; default: break; } } //第一个参数表示严重程度 void LogMessage(int level,const char* format , ...) { //错误的信息 char rightbuffer[1024]; va_list args; va_start(args,format);//取的起始指针,args读取后面具体参数列表 //不想用 va_arg(args)一个个看,我们这里直接处理 vsnprintf(rightbuffer,sizeof(rightbuffer),format,args);//format是"%s,%f,%f",args就是具体对应的值 va_end(args); //记录发生的时间,等等 char leftbuffer[1024]; std::string levelstr = LevelToString(level); std::string currtime = TimeStampExLocalTime(); std::string idstr = std::to_string(getpid()); snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s]",\ levelstr.c_str(),currtime.c_str(),idstr.c_str()); std::string loginfo = leftbuffer; loginfo += rightbuffer; WriteLog(levelstr,loginfo); //printf("%s%s",leftbuffer,rightbuffer); } ~Log(){}; private: int style; std::string filename; };

测试示例:

Main.cc

也可以封装一下

Log lg; class Conf//配置中心 { public: Conf(int style = defultstyle) { lg.Enable(style); } ~Conf() {} }; Conf conf;

本篇结束!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 8:26:29

架构图 Agent 实战:从环境搭建到批量生成的可落地指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 8:21:56

C++类和对象(一)—— 类的定义、封装、对象与this指针

文章目录1. 类的基础知识1.1 类的定义1.1.1class和struct的对比1.1.2 类内定义函数是inline1.1.3 为什么类内函数默认inline&#xff1f;1.2 访问限定符1.2.1 为什么要有访问限定符&#xff1f;1.2.1.1 为什么要有封装性&#xff1f;1.2.2 访问限定符作用的范围1.2.3 访问限定符…

作者头像 李华
网站建设 2026/9/3 8:21:34

基于OpenCV的RGBD视觉里程计实现:从原理到SLAM应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 8:21:08

HOMIE Gen2与人类经验Scaling Law:从概念到落地验证指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 8:19:55

校园二手交易系统:SpringBoot+Vue企业级毕设实战

简介&#xff1a;这是一套面向计算机类专业本科生的高分毕业设计级校园二手交易平台实战项目&#xff0c;适用于毕业设计、课程设计及期末大作业场景&#xff0c;帮助学生快速掌握前后端分离开发全流程。资源包含1151个文件&#xff0c;涵盖443张界面截图与图标资源&#xff08…

作者头像 李华
网站建设 2026/9/3 8:19:40

MATLAB图像去遮挡修复系统:可复现、可调试、可教学

简介&#xff1a;本资源是一套面向数字图像处理课程设计与MATLAB实践学习者的图像去遮挡修复系统&#xff0c;聚焦真实场景中前景栏杆遮挡的智能修复问题&#xff0c;适用于本科高年级或研究生图像处理实验、课程设计及算法对比研究。压缩包共24个文件&#xff0c;含15个核心MA…

作者头像 李华