在并发编程里,自旋锁和互斥锁都是用来保护共享资源的同步工具,但它们的实现思路和适用场景差别很大。理解两者在系统开销上的权衡,核心在于观察锁被持有的时间长短。

自旋锁与互斥锁的基本行为
自旋锁在获取不到锁时,不会立刻让出CPU,而是不断循环检查锁状态,这种行为叫忙等。互斥锁在获取不到锁时,会把线程挂起,交给操作系统调度,等锁释放后再唤醒。
简单自旋锁示例
#include <stdatomic.h>
atomic_int lock = 0;
// 获取自旋锁
void spin_lock(atomic_int* l) {
while (atomic_exchange(l, 1)) {
// 忙等,不断尝试
}
}
// 释放自旋锁
void spin_unlock(atomic_int* l) {
atomic_store(l, 0);
}
互斥锁示例
#include <pthread.h>
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
void* worker(void* arg) {
pthread_mutex_lock(&mtx);
// 临界区
pthread_mutex_unlock(&mtx);
return NULL;
}
从锁持有时间看系统开销
锁持有时间指的是线程进入临界区到离开临界区之间的时长。这个指标直接决定了两种锁的代价。
短持有时间:自旋锁更划算
当临界区代码极少,锁持有时间在几十纳秒到几微秒时,线程切换上下文的开销往往比忙等更大。此时自旋锁避免了一次完整的上下文切换,系统吞吐更高。
长持有时间:互斥锁更合适
如果临界区要读写文件、做网络请求或复杂计算,锁持有时间达到毫秒级,自旋的线程会白白占用CPU核心,导致其他任务饿死。互斥锁让出CPU,整体开销更低。
| 锁类型 | 适用持有时间 | 主要开销 |
|---|---|---|
| 自旋锁 | 极短 | CPU忙等周期 |
| 互斥锁 | 较长 | 上下文切换与调度 |
实际开发中的权衡建议
- 先测量临界区平均耗时,再决定锁类型
- 在用户态高频小操作可用自旋锁,如计数器更新
- 涉及系统调用或IO时优先互斥锁
- 某些语言提供混合锁,短时间自旋失败后再挂起
经验法则:不确定就先用互斥锁,性能瓶颈出现后再针对短临界区改自旋锁。
综上,自旋锁与互斥锁的权衡本质是CPU周期与调度开销的交换。从锁持有时间出发,能更直观地判断系统开销落在哪里,从而写出更高效的并发程序。