优先级反转不是“低优先级线程运行得比高优先级线程久”这么简单。真正的问题是:低优先级线程持有一个高优先级线程必须获得的锁;高优先级线程因此阻塞;另一个中优先级线程却不断抢占持锁者。最高优先级任务最终被一个与它无关的中优先级任务间接挡住,阻塞时间也变得难以预估。
三个线程如何造成反转
设 L、M、H 分别为低、中、高优先级线程。L 先拿到互斥锁并准备访问共享缓冲区;H 到来后需要同一把锁,于是睡眠等待;M 则无需这把锁,持续占用 CPU。没有优先级继承时,L 得不到 CPU 释放锁,H 也无法运行。此时 H 的实际阻塞时间取决于 M 做多久,而不是 L 的临界区本身有多短。
Linux 内核的 rtmutex 将这种优先级继承扩展到等待链:若 L 又在等待另一个锁,优先级可以继续向持锁链传递。PREEMPT_RT 中许多可转换的自旋锁会利用这套机制,因此锁设计直接影响实时性。
用户态锁也要有协议
普通 pthread_mutex_t 默认不一定启用优先级继承。对确实跨优先级共享、且临界区很短的资源,可以在创建锁时明确选择 PTHREAD_PRIO_INHERIT。是否支持及其代价取决于系统和运行环境,所以要在目标机上检查返回值并做压力测试。
1 | pthread_mutexattr_t attr; |
这不是所有锁都该加的魔法属性。大量复杂锁、长 I/O 或回调都放在同一临界区,即使有继承也会让高优先级线程等很久。更好的方案常常是复制快照、无锁队列、双缓冲或把慢操作移到锁外。
降低锁风险的四条规则
- 给共享资源制定唯一的锁顺序,避免嵌套锁形成死锁与长继承链。
- 临界区只做内存读写,不做磁盘、网络、日志或未知时长的函数调用。
- 将“控制路径必须等待的数据”与“后台统计/显示数据”分离。
- 用 trace 或业务日志记录锁等待的最大值,而不是只看平均时间。
优先级继承解决的是“谁应该先拿到 CPU”问题;共享设计决定的是“高优先级任务为什么非要等待”。后者往往更值得先优化。