多线程 忙等待和线程等待的区别
“忙等待”(Busy Waiting / Spinning)和“线程等待”(Blocking / Sleeping / Yielding)是多线程编程中两种截然不同的等待策略,它们在CPU 资源消耗、响应延迟、适用场景等方面有本质区别。
一、忙等待(Busy Waiting / Spinning)
✅ 定义:
线程在等待某个条件满足时,不放弃 CPU,而是持续循环检查条件(例如反复读取一个标志位),直到条件成立。
🔧 示例(伪代码):
while (!flag)
{// 空循环,不断检查 flag
}
// 条件满足,继续执行
或使用 SpinLock、SpinWait 等:
var sw = new SpinWait();
while (!ready)
{sw.SpinOnce(); // 可能短暂让出或插入 pause 指令
}
⚙️ 特点:
| 项目 | 说明 |
|---|---|
| CPU 占用 | 高!线程持续占用 CPU 核心 |
| 响应延迟 | 极低(纳秒~微秒级),一旦条件满足立刻响应 |
| 上下文切换 | 无(不进入内核,不切换线程) |
| 适用场景 | 等待时间极短(如 < 10 微秒) |
| 风险 | 浪费 CPU、可能导致死锁(如在单核 CPU 上) |
💡 优化:
现代实现(如 .NET 的 SpinWait)会:
- 先自旋若干次;
- 若仍未成功,则调用
Thread.Yield()或Thread.Sleep(0)让出 CPU; - 避免在单核系统上完全卡死。
二、线程等待(Blocking / Sleeping)
✅ 定义:
线程在等待时主动放弃 CPU,进入阻塞状态(如等待内核对象信号),由操作系统调度器将其挂起,直到被唤醒。
🔧 示例:
var mre = new ManualResetEvent(false);
mre.WaitOne(); // 线程在此阻塞,不消耗 CPU
// 其他线程调用 mre.Set() 后,本线程被唤醒
其他如:Monitor.Wait()、Task.Wait()、await、Thread.Sleep() 等。
⚙️ 特点:
| 项目 | 说明 |
|---|---|
| CPU 占用 | 几乎为 0(线程不运行) |
| 响应延迟 | 较高(微秒~毫秒级),涉及内核切换和调度 |
| 上下文切换 | 有(进入内核,线程状态变为 Wait/Sleep) |
| 适用场景 | 等待时间较长或不确定 |
| 优势 | 节省 CPU 资源,适合长时间等待 |
⚠️ 注意:
Thread.Sleep()是主动休眠,不是“等待条件”,通常不用于同步(应使用事件、信号量等)。
三、核心对比
| 对比项 | 忙等待(Spinning) | 线程等待(Blocking) |
|---|---|---|
| CPU 使用 | 高(100% 占用一个核心) | 低(几乎为 0) |
| 延迟 | 极低(无切换开销) | 较高(10003000 纳秒以上) |
| 是否进入内核 | 否(用户态) | 是(内核态) |
| 上下文切换 | 无 | 有 |
| 适合等待时间 | 极短(< 几微秒) | 中到长(> 几十微秒) |
| 典型实现 | SpinLock, SpinWait, volatile 循环 | Monitor, AutoResetEvent, Semaphore, await |
四、混合策略:自适应等待(Hybrid Lock)
现代高性能锁(如 Monitor、SemaphoreSlim、ManualResetEventSlim)采用混合策略:
- 先忙等待(自旋若干次)—— 希望在极短时间内获得锁;
- 若失败,则转为线程阻塞—— 避免长期浪费 CPU。
例如 .NET 的 lock 语句底层就是混合锁:
- 先尝试轻量级用户模式同步;
- 若竞争激烈,则升级为内核事件等待。
五、如何选择?
| 场景 | 推荐方式 |
|---|---|
| 临界区极短(如修改一个字段) | 忙等待(Interlocked 或 SpinLock) |
| 通用互斥访问 | lock(混合锁) |
| 异步编程 | SemaphoreSlim.WaitAsync()(非阻塞) |
| 跨线程通知 | AutoResetEvent / TaskCompletionSource |
| 高性能无锁结构 | Interlocked + volatile(避免锁) |
| 长时间等待(如 I/O) | 异步 await,而非任何同步等待 |
总结一句话:
忙等待 = “盯着看,不走开”;线程等待 = “先去睡觉,叫醒我”。
前者快但费电,后者省电但有唤醒延迟。现代系统通常先盯一会儿,再睡觉,取两者之长。
