completion、semaphore 与协作等待
这页讲的是:并发问题不总是“谁先拿锁”,很多时候更像“我先等你把那件事做完”或“这个资源同一时刻允许有限数量的人进入”。学这页的重点,是理解 completion 和 semaphore 更偏协作与限流,而不是单纯互斥。
这块是什么
completion、semaphore 与协作等待,讲的是当两个或多条执行路径之间需要约定“什么时候某件事算完成”“一次最多允许多少人进去”时,Linux 怎样用比互斥锁更贴合语义的同步工具来表达。它们常出现在初始化协作、后台工作完成通知、资源并发数量控制等场景里。
可以把它理解成:有时你需要的不是“把门锁死,只准一个人进”,而是“等装修队说完工再进屋”或者“这间机房同一时间最多进三个人”。
它负责什么
表达“等某事完成”
- 让一条路径等待另一条路径把关键动作做完
- 适合一次性完成通知或阶段性协作
- 把“完成没完成”从口头约定变成正式同步关系
- 让等待和唤醒更贴合业务时序
控制并发进入数量
- 限制某类资源同一时刻被多少路径占用
- 不一定要求完全互斥
- 更适合表达配额和容量上限
- 让同步语义更接近真实资源约束
减少语义错配
- 不是所有等待关系都该拿互斥锁硬表示
- 不同同步原语表达的问题不一样
- 选错工具容易把代码写得既绕又脆弱
- 让“为什么要等”比“怎么卡住别人”更清楚
为什么它不只是“另一种睡眠锁”
| 如果想得太简单 | 会怎样 | 真正关键在哪 |
|---|
| 把 completion 当普通锁 | 会忽略它更像完成信号,而不是谁拥有临界区。 | 它重点在“事情做完没”,不是“谁正在独占”。 |
| 把 semaphore 当 mutex 替身 | 会错过它更适合表示有限并发配额。 | 它不一定只允许一个人进。 |
| 所有协作都靠等待队列手拼 | 代码语义会越来越散,读者不容易看出等待意图。 | 这些原语的价值之一就是把协作模式显式表达出来。 |
| 觉得能等到就行 | 会忽略超时、退出和生命周期可能把协作变复杂。 | 协作等待也要考虑谁可能先走、谁可能永远不来。 |
关键概念
| 概念 | 现在怎么理解 |
|---|
| completion | 一种偏向“某件事完成后通知等待者”的同步工具。 |
| semaphore | 一种更适合表达有限数量并发进入或资源配额的同步工具。 |
| 协作等待 | 等待的重点不是抢临界区,而是等别的路径推进到某个阶段。 |
| 配额控制 | 不是完全禁止并发,而是限制最多允许多少人同时使用。 |
| 完成通知 | 后台或另一执行路径做完后,正式唤醒等待者继续往下走。 |
为什么重要
- 它解释了为什么内核里很多等待关系看起来不像典型锁竞争,而更像阶段协作。
- 很多初始化、退出和后台处理路径,真正难的不是互斥,而是谁等谁、谁通知谁。
- 理解这层后,你会更容易把等待队列、workqueue、超时和资源配额问题串起来。
- 它让你看到同步原语的选择,本质上是在选择系统怎样表达关系。
常见误解
- 误解一:completion 和 semaphore 只是冷门同步工具。实际上它们表达的是很常见的协作语义。
- 误解二:只要有 mutex 就够了。实际上很多等待并不是围绕临界区独占展开。
- 误解三:协作等待只关心睡和醒。实际上它还深受超时、退出和对象生命周期影响。
它不负责什么
- 它不替代所有锁;共享状态并发修改仍常需要互斥或更细同步手段。
- 它不保证等待对象一直活着;完成通知和资源配额仍要和生命周期设计配合。
- 它不自动解决超时与取消语义;这些边界仍需要系统设计明确处理。
和其他模块的关系
| 相关模块 | 关系 |
|---|
| 等待队列、睡眠与唤醒 | 这页可以看作把更具体的协作等待模式,从通用等待机制里继续拆出来。 |
| workqueue 与异步执行 | 后台任务完成后如何通知前台继续,常常会落到这类协作关系上。 |
| 定时器、超时与延迟执行 | 很多协作等待如果太久没结果,还要和超时边界一起考虑。 |
| probe / remove / 生命周期 | 初始化和退出常要等待某些异步动作真正完成,避免半拉子状态。 |
读完这页后,你应该能回答
- 为什么很多同步关系的关键不是“互斥”,而是“等某件事完成”或“限制同时进入人数”?
- 为什么 completion 和 semaphore 往往比单纯 mutex 更贴近某些系统协作语义?
- 为什么协作等待常常会和超时、异步执行和生命周期绑在一起?
后面适合继续问:completion 和等待队列最容易混在哪?为什么 semaphore 更像配额而不只是锁?协作等待在退出路径里最容易踩到哪类永远等不到的问题?