C++ 条件变量 通知 cv.notify_all() 先释放锁再通知
简短的回答是:先释放锁,再通知(notify_one 或 notify_all)通常是更优的选择。 虽然标准允许两种顺序,但“先解锁,后通知”的性能通常更好。
下面我们来详细解释原因和两种方式的区别。
- 先通知,后释放锁 (notify then unlock)
// 假设 mu 是一个 std::mutex, cv 是一个 std::condition_variable
{std::lock_guard<std::mutex> lk(mu); // 持有锁// ... 修改共享数据 ...ready = true;cv.notify_one(); // 1. 先通知(锁仍被持有)
} // 2. lock_guard 超出作用域,自动释放锁
可能发生的问题:性能损耗(“惊群”效应与立即阻塞)
当你在持有锁的情况下调用 notify_one():
- 系统会唤醒一个(或多个,如果是 notify_all)正在等待(waiting)的线程。
- 被唤醒的线程会立即尝试获取与条件变量关联的互斥锁(这是 std::condition_variable::wait 操作的固有步骤)。
- 但是,此时通知线程还持有这把锁!所以被唤醒的线程无法立即获取锁,它会被迫再次进入阻塞(blocking)状态,等待通知线程释放锁。
- 通知线程在 } 处释放锁。
- 被唤醒的线程终于可以再次尝试并成功获取锁。
这个过程导致了一次无谓的上下文切换:被唤醒的线程什么都没做就又被阻塞了。在高性能要求的场景下,这种额外的切换会带来不必要的开销。
- 先释放锁,后通知 (unlock then notify)
{std::unique_lock<std::mutex> lk(mu); // 持有锁// ... 修改共享数据 ...ready = true;lk.unlock(); // 1. 手动先释放锁cv.notify_one(); // 2. 再通知(锁已被释放)
}
// 或者使用一个额外的作用域让 lock_guard 提前释放
{{std::lock_guard<std::mutex> lk(mu);// ... 修改共享数据 ...ready = true;} // 锁在这里被释放cv.notify_one(); // 然后在没有锁的情况下通知
}
优点:性能更优
当你先释放锁再通知:
- 锁已经被释放。
- 调用 notify_one() 唤醒一个等待线程。
- 被唤醒的线程尝试获取互斥锁,此时锁是空闲的,所以它有很大概率能立即成功获取并继续执行,避免了不必要的二次阻塞和上下文切换。
这使得线程调度更加高效。