【操作系统(Linux)】——生产者消费者同步互斥模型
✅ 一、程序功能概述
我们将做的:实现一个经典的「生产者-消费者问题」多线程同步模型的案例,主要用到 循环缓冲区 + POSIX 信号量 sem_t + pthread 多线程库,非常适合理解并发控制、线程通信和缓冲区管理。
案例目标:通过多个生产者线程不断往共享缓冲区中写入数据,多个消费者线程从中读取数据,所有线程对缓冲区的访问必须遵循互斥和同步机制,确保数据完整性与一致性。
案例代码:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
#include <semaphore.h>#define Maxbuf 10
#define TimesOfOp 10
#define historynum 100struct Circlebuf {int read;int write;int buf[Maxbuf];
} circlebuf;sem_t mutex; // 互斥信号量
sem_t empty; // 空槽位信号量
sem_t full; // 满槽位信号量char writehistory[historynum][30];
char readhistory[historynum][30];
char history[historynum][30];
int writehistorycount = 0;
int readhistorycount = 0;
int historycount = 0;void writeCirclebuf(struct Circlebuf *cb, int *value) {cb->buf[cb->write] = *value;sleep(1); // 模拟耗时写入cb->write = (cb->write + 1) % Maxbuf;
}int readCirclebuf(struct Circlebuf *cb) {int value = cb->buf[cb->read];sleep(1); // 模拟耗时读取cb->buf[cb->read] = 0;cb->read = (cb->read + 1) % Maxbuf;return value;
}void sigend(int sig) {exit(0);
}void *productThread(void *arg) {int id = *(int *)arg;int t = TimesOfOp;int writeptr;while (t--) {sem_wait(&empty);sem_wait(&mutex);writeCirclebuf(&circlebuf, &id);writeptr = (circlebuf.write - 1 + Maxbuf) % Maxbuf;sprintf(writehistory[writehistorycount++], "生产者%d:缓冲区%d=%d", id, writeptr, id);sprintf(history[historycount++], "生产者%d:缓冲区%d=%d\n", id, writeptr, id);sem_post(&mutex);sem_post(&full);sleep(1);}return NULL;
}void *consumerThread(void *arg) {int id = *(int *)arg;int t = TimesOfOp;int value, readptr;while (t--) {sem_wait(&full);sem_wait(&mutex);value = readCirclebuf(&circlebuf);readptr = (circlebuf.read - 1 + Maxbuf) % Maxbuf;sprintf(readhistory[readhistorycount++], "消费者%d:缓冲区%d=%d", id, readptr, value);sprintf(history[historycount++], "消费者%d:缓冲区%d=%d\n", id, readptr, value);sem_post(&mutex);sem_post(&empty);sleep(1);}return NULL;
}int main() {int ProdNum = 0, ConsNum = 0;sem_init(&mutex, 0, 1);sem_init(&empty, 0, Maxbuf);sem_init(&full, 0, 0);signal(SIGINT, sigend);signal(SIGTERM, sigend);circlebuf.read = circlebuf.write = 0;for (int i = 0; i < Maxbuf; i++) circlebuf.buf[i] = 0;printf("请输入生产者线程的数目: ");scanf("%d", &ProdNum);printf("请输入消费者线程的数目: ");scanf("%d", &ConsNum);// 分配线程和 ID 数组pthread_t *proThreads = malloc(ProdNum * sizeof(pthread_t));pthread_t *conThreads = malloc(ConsNum * sizeof(pthread_t));int *proIDs = malloc(ProdNum * sizeof(int));int *conIDs = malloc(ConsNum * sizeof(int));// 创建消费者线程for (int i = 0; i < ConsNum; i++) {conIDs[i] = i + 1;if (pthread_create(&conThreads[i], NULL, consumerThread, &conIDs[i]) != 0) {perror("Create consumer thread error");exit(EXIT_FAILURE);}}// 创建生产者线程for (int i = 0; i < ProdNum; i++) {proIDs[i] = i + 1;if (pthread_create(&proThreads[i], NULL, productThread, &proIDs[i]) != 0) {perror("Create producer thread error");exit(EXIT_FAILURE);}}// 等待所有线程完成for (int i = 0; i < ProdNum; i++) pthread_join(proThreads[i], NULL);for (int i = 0; i < ConsNum; i++) pthread_join(conThreads[i], NULL);// 打印读写对比历史printf("\n========= 读写历史对比表 =========\n");int max = (writehistorycount > readhistorycount) ? writehistorycount : readhistorycount;for (int i = 0; i < max; i++) {printf("%-30s | %-30s\n",(i < writehistorycount) ? writehistory[i] : " ",(i < readhistorycount) ? readhistory[i] : " ");}// 打印操作历史记录printf("\n*************缓冲池的操作历史为:*************\n");for (int i = 0; i < historycount; i++) {printf("%s", history[i]);}// 资源清理sem_destroy(&mutex);sem_destroy(&empty);sem_destroy(&full);free(proThreads);free(conThreads);free(proIDs);free(conIDs);return 0;
}
下面将系统性地讲解该代码的结构、功能、关键技术点,理解每一块的作用与原理。
🔧 二、关键结构与全局变量
🔹 1. 缓冲区结构体
struct Circlebuf {int read;int write;int buf[Maxbuf];
} circlebuf;
buf[]:固定大小的缓冲区,大小为Maxbuf(10)read:读指针(消费者读取位置)write:写指针(生产者写入位置)
构成一个循环缓冲队列,写满了从头开始,读完了也从头读。
🔹 2. 信号量定义
sem_t mutex; // 互斥访问缓冲区
sem_t empty; // 剩余可写槽位数(空位)
sem_t full; // 可读槽位数(数据个数)
三个信号量分别控制:
mutex: 对缓冲区访问的互斥锁empty: 控制写入前必须有空位full: 控制读取前必须有数据
🔹 3. 记录写/读操作历史
char writehistory[historynum][30]; // 写入历史
char readhistory[historynum][30]; // 读取历史
char history[historynum][30]; // 所有历史记录
用于调试输出,记录每一次写入或读取的内容、缓冲区位置和线程编号。
🔄 三、核心操作函数
🔹 1. 写入缓冲区 writeCirclebuf
void writeCirclebuf(struct Circlebuf *circlebuf,int *value){circlebuf->buf[circlebuf->write] = (*value);sleep(1); // 模拟耗时circlebuf->write = (circlebuf->write + 1) % Maxbuf;
}
将值写入当前写指针处,并更新写指针位置(循环回绕)。
🔹 2. 读取缓冲区 readCirclebuf
int readCirclebuf(struct Circlebuf *circlebuf){int value = circlebuf->buf[circlebuf->read];sleep(1);circlebuf->buf[circlebuf->read] = 0;circlebuf->read = (circlebuf->read + 1) % Maxbuf;return value;
}
读取并清空当前位置的值,并更新读指针位置。
🔹 3. 生产者线程函数
void *productThread(void *i)
工作流程:
sem_wait(&empty):缓冲区空位数 > 0 时继续sem_wait(&mutex):互斥锁保护缓冲区- 执行写操作
- 保存历史记录
sem_post(&mutex):释放互斥锁sem_post(&full):可读数量 +1- 每次
sleep(1)方便观察
🔹 4. 消费者线程函数
void *consumerThread(void *i)
工作流程与生产者相反:
sem_wait(&full):缓冲区有数据可读sem_wait(&mutex):互斥锁保护缓冲区- 执行读取操作
- 保存历史记录
sem_post(&mutex):释放互斥锁sem_post(&empty):空位 +1sleep(1)观察用
🧵 四、主函数 main
1️⃣ 初始化
sem_init(&mutex,0,1);
sem_init(&empty,0,Maxbuf);
sem_init(&full,0,0);
初始化三个信号量:
mutex = 1:用于互斥访问缓冲区empty = Maxbuf:初始全是空的full = 0:没有数据可读
设置 SIGINT、SIGTERM 信号处理为 exit(0),用于 Ctrl+C 优雅退出。
2️⃣ 创建线程
根据输入创建多个生产者线程和消费者线程:
pthread_create(&proid[i], NULL, productThread, &pro[i-1]);
注意:你在创建线程时有 数组越界风险(见补充建议),见最后改进建议。
3️⃣ 输出对比
sleep((ConsNum+ProdNum)*10);
等待足够时间让所有线程完成操作。
之后输出:
- 每个生产者写入什么
- 每个消费者读取什么
- 完整的缓冲区操作历史
✅ 五、关键同步机制总结
| 操作类型 | 使用的信号量 | 作用说明 |
|---|---|---|
| 写(生产者) | sem_wait(empty) | 等待有空位写入 |
| 写 | sem_wait(mutex) | 加互斥锁,保护缓冲区 |
| 写 | sem_post(full) | 写完后增加可读数量 |
| 读(消费者) | sem_wait(full) | 等待有数据可读 |
| 读 | sem_wait(mutex) | 加互斥锁,保护缓冲区 |
| 读 | sem_post(empty) | 读完后释放空位 |
⚠️ 六、代码可优化建议
- ✅ 线程数组越界问题:
pthread_create(&proid[i], ...) // i 从 1 到 N
应改为:
for(i = 0; i < ProdNum; i++) {pro[i] = i + 1;pthread_create(&proid[i], ...);
}
否则 proid[i] 会越界访问未分配内存。
- ✅ 增加线程 join 回收资源
可加入:
for (i = 0; i < ProdNum; i++) pthread_join(proid[i], NULL);
for (i = 0; i < ConsNum; i++) pthread_join(conid[i], NULL);
- ✅ 可读性优化
输出用 printf("\033[1;32m...") 等 ANSI 转义码加颜色,便于区分线程。
✅ 七、运行效果示意(节选)
请输入生产者线程的数目 :2
请输入消费者线程的数目 :2生产者1:缓冲区0=1 | 消费者1:缓冲区0=1
生产者2:缓冲区1=2 | 消费者2:缓冲区1=2
生产者1:缓冲区2=1 | 消费者1:缓冲区2=1
...*************缓冲池的操作历史为:******************
生产者1:缓冲区0=1
消费者1:缓冲区0=1
...
🎯 总结
| 技术点 | 说明 |
|---|---|
| 多线程并发 | 使用 pthread_create 创建多个生产者与消费者 |
| 循环缓冲区 | 通过 read/write 指针实现 FIFO 缓冲结构 |
| 同步机制 | 使用信号量 empty/full/mutex 避免竞争 |
| 信号处理 | 捕获 SIGINT/SIGTERM,安全退出 |
| 调试可视化 | 使用 history 记录生产消费过程 |
相关文章:
【操作系统(Linux)】——生产者消费者同步互斥模型
✅ 一、程序功能概述 我们将做的:实现一个经典的「生产者-消费者问题」多线程同步模型的案例,主要用到 循环缓冲区 POSIX 信号量 sem_t pthread 多线程库,非常适合理解并发控制、线程通信和缓冲区管理。 案例目标:通过多个生产…...
SQL ③-基本语法
SQL基本语法 表操作 创建表 CREATE TABLE table_name (column1 datatype constraint,column2 datatype constraint,column3 datatype constraint,... );删除表 DROP [TEMPORARY] TABLE [IF EXISTS] table_name [, table_name...];TEMPORARY:表示临时表ÿ…...
【Pandas】pandas DataFrame bool
Pandas2.2 DataFrame Conversion 方法描述DataFrame.astype(dtype[, copy, errors])用于将 DataFrame 中的数据转换为指定的数据类型DataFrame.convert_dtypes([infer_objects, …])用于将 DataFrame 中的数据类型转换为更合适的类型DataFrame.infer_objects([copy])用于尝试…...
2025年3月全国青少年软件编程等级考试(Python五级)试卷及答案
2025.03电子学会 全国青少年软件编程等级考试(Python五级)试卷 一、单选题 1.以下哪个选项不是Python中的推导式?( ) A.列表推导式 B.字典推导式 C.集合推导式 D.元组推导式 2.以下Python代码的返回结果是?( ) [x**2 for…...
esp32cam -> 服务器 | 手机 -> 服务器 直接服务器传输图片
服务器先下载python : 一、Python环境搭建(CentOS/Ubuntu通用) 一条一条执行 安装基础依赖 # CentOS sudo yum install gcc openssl-devel bzip2-devel libffi-devel zlib-devel # Ubuntu sudo apt update && sudo apt install b…...
豆浆机语音提示芯片方案:基于可远程在线更换语音的WT2003H-16S芯片
随着智能家居概念的普及,消费者对家电产品的智能化、便捷性提出了更高要求。豆浆机作为厨房常用电器,其操作便捷性和用户体验直接影响市场竞争力。传统豆浆机多依赖指示灯或简单蜂鸣器提示用户操作状态,信息传递单一且无法满足个性化需求。 在…...
解密工业控制柜:认识关键硬件(PLC)
前言 作为一名视觉开发工程师,我们不仅要做到做好自己的工作,我们更需要在工业现场学习更多知识,最近网上流传很多,“教会徒弟,饿死师傅”;在自动化行业中,在项目下来很忙的时候,我们…...
【嵌入式系统设计师】知识点:第11 章 嵌入式系统设计案例分析
提示:“软考通关秘籍” 专栏围绕软考展开,全面涵盖了如嵌入式系统设计师、数据库系统工程师、信息系统管理工程师等多个软考方向的知识点。从计算机体系结构、存储系统等基础知识,到程序语言概述、算法、数据库技术(包括关系数据库、非关系型数据库、SQL 语言、数据仓库等)…...
记录一次SSH和SFTP服务分离后文件上传权限问题
开门见山 因服务器安全需求,需要将ssh和sftp服务分离,并创建一个用户组sftpuser::sftp,根目录权限均正常。用户sftpuser仅能通过sftp访问服务器,不能通过ssh访问服务器。但是,ssh应用用户appuser::sftp通过sftp建立链…...
【深度解析】SkyWalking 10.2.0版本安全优化与性能提升实战指南
前言 Apache SkyWalking 作为云原生可观测性领域的佼佼者,在微服务架构监控中扮演着至关重要的角色。然而,官方版本在安全性、镜像体积和功能扩展方面仍有优化空间。本文将分享一套完整的 SkyWalking 10.2.0 版本优化方案,从安全漏洞修复到镜…...
面向大模型的开发框架LangChain
这篇文章会带给你 如何使用 LangChain:一套在大模型能力上封装的工具框架如何用几行代码实现一个复杂的 AI 应用面向大模型的流程开发的过程抽象 文章目录 这篇文章会带给你写在前面LangChain 的核心组件文档(以 Python 版为例)模型 I/O 封装…...
pip install pytrec_eval失败的解决方案
1、问题描述 在使用华为云 notebook 的时候,想要: !pip install transformer结果失败,阅读报错后,疑似是 pytrec_eval 库的下载问题。 于是,单独尝试: !pip install pytrec_eval发现确实是这个库安装失…...
Easysearch VS Opensearch 数据写入与存储性能对比
本文记录 Easysearch 和 Opensearch 数据写入和数据存储方面的性能对比。 准备 压测工具:INFINI Loadgen 对比版本: Easysearch 1.11.1(lucene 8.11.4)Opensearch 2.19.1(lucene 9.12.1) 节点 JVM 配置…...
【Proteus仿真】【32单片机-A009】矩阵按键系统设计
目录 一、主要功能 二、使用步骤 三、硬件资源 四、软件设计 五、实验现象 联系作者 一、主要功能 1、按键值与LCD显示 2、矩阵按键 二、使用步骤 系统运行后,LCD1602显示当前的按键值; 当按下不同按键后显示屏更新对应的按键值。 三、硬件资…...
考研单词笔记 2025.04.09
act v表现,行动,做事,扮演,充当,担任,起作用n行为,行动,法案,法令 action n行为,行动 behave v表现,行事,守规矩,举止端…...
用一个实际例子快速理解MCP应用的工作步骤
已经有很多的文章介绍MCP server,MCP Client工作原理,这里不做太多介绍。但是很多介绍都只是侧重介绍概念,实际的工作原理理解起来对初学者还是不太友好。本文以一个智能旅游咨询系统为例,详细说明在利用 Model Context Protocol&…...
TCP 和 UDP 可以使用同一个端口吗?
TCP 和 UDP 可以使用同一个端口吗? 前言 在深入探讨 TCP 和 UDP 是否可以使用同一个端口之前,我们首先需要理解网络通信的基本原理。网络通信是一个复杂的过程,涉及到多个层次的协议和机制。在 OSI 模型中,传输层是负责端到端数…...
探索原生JS的力量:自定义实现类似于React的useState功能
1.写在前面 本方案特别适合希望在历史遗留的原生JavaScript项目中实现简单轻量级数据驱动机制的开发者。无需引入任何框架或第三方库,即可按照此方法封装出类似于React中useState的功能,轻松为项目添加状态管理能力,既保持了项目的轻量性&am…...
探索 Shell 中的扩展通配符:从 Bash 到 Zsh
在 Unix 系统中,通配符(globbing)是 shell 的核心功能,用于快速匹配文件或目录。基础通配符(如 *、?、[])虽简单实用,但在复杂场景下往往力不从心。为此,许多现代 shell 提供了“扩…...
封装方法的辨析
equals //字符串 str1.equals(str2); //list的两个实现类 list1.equals(list2); //map的两个实现类 //比较所有的键值对是否相同 map1.equals(map2); //数组(包括string类型) //比较内容是否相同 Arrays.equals(array1, array2); contains 基本都有…...
[leetcode]判断质数
一.判断质数 1.1 什么是质数 质数(素数)就是只可以被自己和1整除的数叫做素数/质数 1.2判断方法 #include<bits/stdc.h> using namespace std; bool isPrime(int num) { if(num < 1) { return false;//a number less of …...
在Flutter中使用BottomNavigationBar和IndexedStack可以实现一个功能完整的底部导航栏
在Flutter中,使用BottomNavigationBar和IndexedStack可以实现一个功能完整的底部导航栏。BottomNavigationBar用于显示底部的导航按钮,而IndexedStack则用于管理页面的切换,确保每个页面的状态得以保留(即页面不会因为切换而重新构…...
HBuilder运行uni-app程序报错【Error: listen EACCES: permission denied 0.0.0.0:5173】
一、错误提示: 当使用HBuilder运行uni-app项目的时候提示了如下错误❌ 15:11:03.089 项目 project 开始编译 15:11:04.404 请注意运行模式下,因日志输出、sourcemap 以及未压缩源码等原因,性能和包体积,均不及发行模式。 15:11:04…...
聊透多线程编程-线程基础-3.C# Thread 如何从非UI线程直接更新UI元素
目录 1. 使用 Control.Invoke 或 Control.BeginInvoke(Windows Forms) 2. 使用 Dispatcher.Invoke 或 Dispatcher.BeginInvoke(WPF) 3. 使用 SynchronizationContext 桌面应用程序(如 Windows Forms 或 WPF…...
VMware Fusion Pro 13 for Mac虚拟机
VMware Fusion Pro 13 for Mac虚拟机 文章目录 VMware Fusion Pro 13 for Mac虚拟机一、介绍二、效果下载 一、介绍 VMware Fusion Pro for Mac,是一款mac虚拟机软件,跟Parallels Desktop一样,都可以让你的 Mac 同时运行一个或多个不同的操作…...
7.第二阶段x64游戏实战-string类
免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动! 本次游戏没法给 内容参考于:微尘网络安全 上一个内容:7.第二阶段x64游戏实战-分析人物属性 string类是字符串类,在计算机中…...
【debug莫名其妙跑飞了】
现象:就是在初始化汇编里跑飞了,也可能运行起来时钟不对 原因:调试器调试程序时会执行reset复位,reset没有正确执行。 细节决定成败,事出反常必有妖,忽略的小卡拉米最后能玩死你啊...
【Git 常用操作指令指南】
一、初始化与配置 1. 设置全局账户信息 git config --global user.name "用户名" # 设置全局用户名 git config --global user.email "邮箱" # 设置全局邮箱 --global 表示全局生效,若需针对单个仓库配置,可省略该参数 2.…...
基础知识补充篇:什么是DAPP前端连接中的provider
专栏:区块链入门到放弃查看目录-CSDN博客文章浏览阅读352次。为了方便查看将本专栏的所有内容列出目录,按照顺序查看即可。后续也会在此规划一下后续内容,因此如果遇到不能点击的,代表还没有更新。声明:文中所出观点大多数源于笔者多年开发经验所总结,如果你想要知道区块…...
openssl源码分析之加密模式(modes)
openssl实现分组加密模式(例如AES128-CBC的CBC部分)的模块名字叫做modes,源代码位于 https://gitee.com/gh_mirrors/openssl/tree/master/crypto/modes 博主又打不开github了TT,只能找个gitee镜像 头文件是modes.h。 该模块目前…...
