unique_lock 与 condition_variable
定义与作用
unique_lock 是灵活的 RAII 锁包装器,支持延迟加锁、提前解锁和锁转移。condition_variable 提供线程间等待/通知机制。
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
// 等待线程
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; }); // 等待直到 ready==true
// 通知线程
{
std::lock_guard<std::mutex> lock(mtx);
ready = true;
}
cv.notify_one();
核心原理
条件变量工作流
unique_lock vs lock_guard
完整示例
示例一:飞翔科技任务队列(生产者-消费者)
场景说明:白歌实现飞翔科技后台任务调度系统。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <string>
#include <chrono>
#include <random>
#include <sstream>
std::mutex coutMutex; // 仅用于同步输出
class TaskQueue {
public:
void push(const std::string& task) {
{
std::lock_guard<std::mutex> lock(mtx_);
queue_.push(task);
}
cv_.notify_one(); // 通知一个等待的消费者
}
std::string pop() {
std::unique_lock<std::mutex> lock(mtx_);
// 带谓词的 wait:自动处理虚假唤醒
cv_.wait(lock, [this] { return !queue_.empty() || stopped_; });
if (stopped_ && queue_.empty())
return ""; // 停止信号
auto task = queue_.front();
queue_.pop();
return task;
}
void stop() {
{
std::lock_guard<std::mutex> lock(mtx_);
stopped_ = true;
}
cv_.notify_all(); // 唤醒所有等待线程以退出
}
size_t size() const {
std::lock_guard<std::mutex> lock(mtx_);
return queue_.size();
}
private:
mutable std::mutex mtx_;
std::condition_variable cv_;
std::queue<std::string> queue_;
bool stopped_ = false;
};
void producer(TaskQueue& q, int id) {
std::vector<std::string> tasks = {
"编译项目", "运行测试", "部署服务",
"备份数据库", "清理日志", "发送报告",
};
std::mt19937 rng(id);
std::uniform_int_distribution<int> dist(100, 500);
for (const auto& t : tasks) {
std::this_thread::sleep_for(std::chrono::milliseconds(dist(rng)));
std::ostringstream oss;
oss << "[P" << id << "] " << t;
q.push(oss.str());
{
std::lock_guard<std::mutex> lock(coutMutex);
std::cout << " 生产者 " << id << " 发布: " << t
<< " (队列: " << q.size() << ")\n";
}
}
}
void consumer(TaskQueue& q, int id) {
std::mt19937 rng(id + 100);
std::uniform_int_distribution<int> dist(200, 800);
while (true) {
auto task = q.pop();
if (task.empty()) break; // 停止信号
{
std::lock_guard<std::mutex> lock(coutMutex);
std::cout << "消费者 " << id << " 开始: " << task << "\n";
}
// 模拟处理任务
std::this_thread::sleep_for(std::chrono::milliseconds(dist(rng)));
{
std::lock_guard<std::mutex> lock(coutMutex);
std::cout << "消费者 " << id << " 完成: " << task << "\n";
}
}
}
int main() {
std::cout << "=== 飞翔科技任务调度系统 ===\n\n";
TaskQueue queue;
std::vector<std::thread> producers;
for (int i = 1; i <= 3; ++i)
producers.emplace_back(producer, std::ref(queue), i);
std::thread c1(consumer, std::ref(queue), 1);
std::thread c2(consumer, std::ref(queue), 2);
for (auto& t : producers) t.join();
// 所有生产者完成,发送停止信号
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "\n所有任务已发布,发送停止信号...\n";
queue.stop();
c1.join();
c2.join();
std::cout << "所有消费者已退出,调度系统关闭\n";
}
预期输出:
=== 飞翔科技任务调度系统 ===
生产者 1 发布: 编译项目 (队列: 1)
消费者 1 开始: [P1] 编译项目
生产者 2 发布: 编译项目 (队列: 1)
消费者 2 开始: [P2] 编译项目
...
消费者 1 完成: [P1] 编译项目
...
所有任务已发布,发送停止信号...
所有消费者已退出,调度系统关闭
逐段分析:
cv.wait(lock, pred)等价于while(!pred()) cv.wait(lock),自动处理虚假唤醒notify_one()唤醒一个等待线程;notify_all()唤醒所有unique_lock在wait时可以自动释放/重新获取锁stopped_标志 +notify_all()是优雅关闭的经典模式
示例二:虚假唤醒演示与对比
场景说明:黄俪演示为什么带谓词的 wait 是必要的。
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data;
bool ready = false;
// ❌ 错误方式:容易受虚假唤醒影响
void consumerWrong(int id) {
std::unique_lock<std::mutex> lock(mtx);
if (data.empty()) { // if 而非 while
cv.wait(lock); // 不带谓词
}
int val = data.front();
data.pop();
std::cout << "消费者 " << id << " (错误) 拿到: " << val << "\n";
}
// ✅ 正确方式:带谓词的 wait
void consumerRight(int id) {
std::unique_lock<std::mutex> lock(mtx);
// 带谓词:等价于 while(!pred) wait()
cv.wait(lock, [] { return !data.empty(); });
int val = data.front();
data.pop();
std::cout << "消费者 " << id << " (正确) 拿到: " << val << "\n";
}
int main() {
std::cout << "=== 虚假唤醒对比 ===\n\n";
// 演示正确方式
std::cout << "--- 正确方式 ---\n";
std::thread c1(consumerRight, 1);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
{
std::lock_guard<std::mutex> lock(mtx);
data.push(42);
}
cv.notify_one();
c1.join();
// 总结
std::cout << "\n--- 关键区别 ---\n";
std::cout << "错误: 使用 if 检查条件 + 无谓词 wait\n";
std::cout << " - 虚假唤醒后不会重新检查条件\n";
std::cout << " - 可能访问空队列导致未定义行为\n";
std::cout << "正确: 使用带谓词的 cv.wait(lock, pred)\n";
std::cout << " - 内部等价于 while(!pred()) wait(lock)\n";
std::cout << " - 自动处理虚假唤醒\n";
}
预期输出:
=== 虚假唤醒对比 ===
--- 正确方式 ---
消费者 1 (正确) 拿到: 42
--- 关键区别 ---
错误: 使用 if 检查条件 + 无谓词 wait
- 虚假唤醒后不会重新检查条件
- 可能访问空队列导致未定义行为
正确: 使用带谓词的 cv.wait(lock, pred)
- 内部等价于 while(!pred()) wait(lock)
- 自动处理虚假唤醒
逐段分析:
- 虚假唤醒是 POSIX 规范允许的行为:即使没有
notify,wait也可能返回 cv.wait(lock, pred)内部实现为while(!pred()) wait(lock)- 永远使用带谓词的
wait,除非有特殊原因
易错场景与面试考点
易错场景
1. 不带谓词的 wait
// ❌
cv.wait(lock); // 虚假唤醒后不检查条件
// ✅
cv.wait(lock, [] { return ready; });
2. 在 lock 外修改共享数据
// ❌
{
std::lock_guard lock(mtx);
data.push(x);
} // 解锁
cv.notify_one(); // 在解锁后通知 → 被通知线程可能看到旧状态
// ✅ 解锁后通知也安全,因为 wait 会重新检查谓词
// 但在锁内通知可避免"惊群"开销
{
std::lock_guard lock(mtx);
data.push(x);
cv.notify_one(); // 锁内通知(建议)
}
3. 忘记处理停止条件
// ❌ 消费者永远等待
cv.wait(lock, [] { return !queue.empty(); }); // 队列空时永久阻塞
// ✅ 加上停止标志
cv.wait(lock, [] { return !queue.empty() || stopped; });
面试考点
| 考点 | 要点 |
|---|---|
| unique_lock vs lock_guard | unique_lock 更灵活:延迟加锁、手动解锁、可转移 |
| condition_variable | 等待/通知机制,必须配合 mutex 使用 |
| wait 两种形式 | 无谓词(需手动 while)、带谓词(推荐) |
| 虚假唤醒 | OS 可能无故唤醒,必须 while 检查条件 |
| notify_one vs notify_all | 一个 vs 全部等待线程 |
| 生产者消费者模式 | 经典并发模型,条件变量 + 队列 |
| unique_lock 构造选项 | adopt_lock / defer_lock / try_to_lock |
| unique_lock 可移动 | 支持 move,不可 copy |