锁竞争发生时,系统有两种基本选择:继续占 CPU 反复检查锁是否释放,或把线程挂起,让出 CPU 等待唤醒。自旋锁选择前者,换取极低的交接延迟;互斥锁通常选择后者,减少浪费却引入调度与唤醒开销。正确选择不取决于“自旋总是快”或“睡眠总是省”,而取决于临界区长度、持锁者是否正在运行、当前上下文能否睡眠以及竞争强度。
自旋为什么可能比 mutex 更糟
若持锁线程正在另一个 CPU 上运行,且只会再持锁几十条指令,自旋可以避免一次睡眠和唤醒,确实划算。但若持锁线程被抢占、正在等待 I/O、跑在同一 CPU,或临界区会执行复杂计算,自旋者只是在白白烧 CPU,还可能让持锁者更难获得运行机会。
内核自旋锁用于中断/softirq 等不能睡眠的上下文;用户态普通应用一般优先选择 pthread_mutex,因为 futex 路径能在无竞争时很快、在竞争时睡眠。用户态自旋锁只有在测量证明竞争极短、CPU 核数充足且没有优先级问题时才值得考虑。
先量临界区,而不是先改 API
记录锁等待时间、持锁时间、竞争次数和持锁线程的状态。很多“锁慢”问题其实是临界区内做了字符串格式化、磁盘日志、RPC、内存分配或回调。将这些操作移到锁外,通常比把 mutex 改成自旋锁更有效。
1 | 错误模式:lock -> 查数据库/写日志/调用回调 -> unlock |
若需要保护的是一个小状态快照,可用双缓冲、原子变量或单生产者单消费者队列降低竞争;若是多线程共享复杂对象,先写清对象生命周期和锁顺序,再谈微优化。
与实时任务一起使用时
实时线程不应在不可控时长的锁上等待。对于跨优先级互斥,考虑优先级继承;对于控制循环,尽量让数据由后台线程预处理成可快速读取的快照。不要用自旋来“保证不睡眠”,因为它可能只是把不确定等待变成高功耗、低可控的忙等。
锁的最佳性能常常来自不需要竞争:通过分片、所有权转移、消息传递和清晰的处理阶段,让大多数操作根本不去抢同一把锁。