1737 字
9 分钟
POSIX无名信号量深度解析:用流水线思想玩转线程同步
POSIX无名信号量深度解析:用流水线思想玩转线程同步
🎯 开篇:为什么你需要信号量?
想象一下,你开了一家手工披萨店。厨房里三个师傅分别负责:
- 师傅A:和面做饼底
- 师傅B:涂抹酱料和芝士
- 师傅C:烘烤和包装
如果三个师傅各干各的,会发生什么?
- 师傅B可能把酱料涂在还没做好的饼底上
- 师傅C可能把没加酱料的饼底直接放进烤箱
- 顾客拿到的是一团糟的”披萨”
这就是线程同步要解决的问题!在多线程程序中,我们需要一种机制来协调不同线程的执行顺序,就像协调三个披萨师傅的工作流程一样。
🔧 信号量是什么?
信号量(Semaphore)就像厨房里的订单铃铛:
- 当师傅A完成饼底制作,他会按一下铃铛(信号量+1)
- 师傅B听到铃铛后,开始涂抹酱料,完成后按下一个铃铛(信号量+1)
- 师傅C听到铃铛后,开始烘烤包装
在编程世界里,信号量是一个计数器,用来控制多个线程对共享资源的访问。
🎨 无名信号量 vs 有名信号量
POSIX信号量分为两类:
| 类型 | 特点 | 使用场景 |
|---|---|---|
| 无名信号量 | 内存中的信号量,随进程结束而消失 | 线程间同步(同一进程内) |
| 有名信号量 | 有名字的信号量,可跨进程使用 | 进程间同步 |
今天的主角是无名信号量,它就像厨房里的内部通讯系统,只在店铺内部使用。
💻 代码实战:电子工厂流水线
让我们用一个电子工厂的例子来理解信号量的使用。工厂有三个工作站:
#include <stdio.h>#include <pthread.h>#include <unistd.h>#include <semaphore.h>
// 定义三个信号量,对应三个工作环节sem_t sem1_welding; // 焊接元器件环节sem_t sem2_check; // 检查元器件环节sem_t sem3_packing; // 打包发货环节🔍 信号量初始化详解
// 初始化信号量的正确姿势sem_init(&sem1_welding, 0, 1); // 初始值为1,表示焊接环节可以开始sem_init(&sem2_check, 0, 0); // 初始值为0,等待焊接完成sem_init(&sem3_packing, 0, 0); // 初始值为0,等待检查完成参数解释:
- 参数1:
&sem1_welding- 信号量指针 - 参数2:
0- 0表示线程间共享,非0表示进程间共享 - 参数3:
1- 信号量初始值
🏭 三个工作线程的实现
1️⃣ 焊接工作站
void* Ttask1_Welding(void* arg){ while (1) { // P操作:等待可以开始焊接的信号 sem_wait(&sem1_welding);
printf("======================\n"); printf("(1)、焊接元器件!\n");
// V操作:通知检查环节可以开始了 sem_post(&sem2_check);
sleep(1); // 模拟工作时间 }}2️⃣ 质检工作站
void* Ttask2_Check(void* arg){ while (1) { // P操作:等待焊接完成的信号 sem_wait(&sem2_check);
printf("(2)、检查元器件!\n");
// V操作:通知打包环节可以开始了 sem_post(&sem3_packing);
sleep(1); // 模拟工作时间 }}3️⃣ 包装工作站
void* Ttask3_Packing(void* arg){ while (1) { // P操作:等待检查完成的信号 sem_wait(&sem3_packing);
printf("(3)、打包发货!\n");
// V操作:通知焊接环节可以开始下一轮了 sem_post(&sem1_welding);
sleep(1); // 模拟工作时间 }}🎮 主函数:启动流水线
int main(int argc, char const *argv[]){ pthread_t task1_welding_pid; pthread_t task2_check_pid; pthread_t task3_packing_pid;
// 创建三个工作线程 pthread_create(&task1_welding_pid, NULL, Ttask1_Welding, NULL); pthread_create(&task2_check_pid, NULL, Ttask2_Check, NULL); pthread_create(&task3_packing_pid, NULL, Ttask3_Packing, NULL);
while(1); // 主线程保持运行
// 清理工作(实际上不会执行到) sem_destroy(&sem1_welding); sem_destroy(&sem2_check); sem_destroy(&sem3_packing);
return 0;}🔄 P操作和V操作的底层原理
📉 P操作(sem_wait)
sem_wait(&sem1_welding);内部实现:
- 检查信号量值是否大于0
- 如果大于0,信号量值减1,继续执行
- 如果等于0,线程阻塞等待
就像工人说:“有材料吗?有的话我拿走一个开始工作,没有的话我等会儿。”
📈 V操作(sem_post)
sem_post(&sem2_check);内部实现:
- 信号量值加1
- 如果有线程在等待,唤醒其中一个
就像工人说:“我完成工作了,放一个成品到传送带上!”
🎪 运行效果分析
程序运行后会看到:
======================(1)、焊接元器件!(2)、检查元器件!(3)、打包发货!======================(1)、焊接元器件!(2)、检查元器件!(3)、打包发货!...完美实现了:
- 焊接 → 检查 → 打包的顺序执行
- 每个环节等待前一个环节完成
- 循环往复,永不停歇
⚠️ 常见踩坑指南
💥 坑点1:忘记初始化信号量
// ❌ 错误:使用未初始化的信号量sem_t sem;sem_wait(&sem); // 未定义行为!
// ✅ 正确:先初始化再使用sem_init(&sem, 0, 1);sem_wait(&sem);💥 坑点2:信号量值设置错误
// ❌ 错误:所有信号量都初始化为1sem_init(&sem1, 0, 1);sem_init(&sem2, 0, 1); // 应该为0!sem_init(&sem3, 0, 1); // 应该为0!
// 这样会导致所有线程同时开始,没有同步效果💥 坑点3:忘记销毁信号量
// ❌ 错误:程序退出前不销毁信号量// 可能导致资源泄漏
// ✅ 正确:使用完销毁信号量sem_destroy(&sem);💥 坑点4:P/V操作不匹配
// ❌ 错误:P操作多于V操作sem_wait(&sem); // P操作sem_wait(&sem); // 又一个P操作// 信号量可能变成负值,线程永久阻塞
// ✅ 正确:P/V操作要成对出现sem_wait(&sem); // P操作// ... 做一些工作 ...sem_post(&sem); // V操作🎨 生活化类比总结
| 编程概念 | 生活类比 | 关键点 |
|---|---|---|
| 信号量 | 餐厅座位计数器 | 控制同时就餐人数 |
| P操作 | 顾客进店 | 座位数减1,没座位就排队 |
| V操作 | 顾客离店 | 座位数加1,通知排队顾客 |
| 初始值 | 餐厅总座位数 | 决定最大并发量 |
🚀 进阶思考
🤔 如果改变初始值会怎样?
sem_init(&sem1_welding, 0, 3); // 改为3这样会有3个产品同时进入流水线,形成并行流水线!
🤔 如何实现生产者-消费者模式?
sem_t empty; // 空缓冲区数量sem_t full; // 满缓冲区数量sem_t mutex; // 互斥信号量🤔 信号量 vs 互斥锁
| 特性 | 信号量 | 互斥锁 |
|---|---|---|
| 用途 | 同步 | 互斥 |
| 取值 | 0~N | 0~1 |
| 所有权 | 无 | 有 |
| 灵活性 | 高 | 低 |
🎯 总结:信号量使用三步走
- 初始化:设置合适的初始值
- 使用:P/V操作成对出现
- 清理:用完记得销毁
记住:信号量就像交通信号灯,让多线程程序有条不紊地运行,避免”交通事故”!
📚 延伸阅读
想要深入学习?可以探索:
- POSIX有名信号量(跨进程同步)
- System V信号量
- 条件变量(另一种同步机制)
- 读写锁(读者-写者问题)
编程就像做菜,信号量就是你的厨房计时器。用好了,多线程程序就能像米其林餐厅一样井然有序! 🍳✨
POSIX无名信号量深度解析:用流水线思想玩转线程同步
https://demo-firefly.netlify.app/posts/posix-unnamed-semaphores-thread-synchronization/ 最后更新于 2024-06-20,距今已过 519 天
部分内容可能已过时