您误解了您的代码的作用。
您在// 1 上的代码完全不受阻塞。 condition_variables 可以(并且将会!)有虚假的唤醒——他们可以毫无理由地唤醒。
您有责任检查唤醒是否虚假。
正确使用condition_variable 需要三件事:
- 一个
condition_variable
- 一个
mutex
-
mutex 保护的一些数据
互斥锁保护的数据被修改(在mutex下)。然后(mutex 可能未启用),condition_variable 会收到通知。
在另一端,您锁定mutex,然后等待条件变量。当您醒来时,您的mutex 被重新锁定,您可以通过查看mutex 保护的数据来测试唤醒是否是虚假的。如果它是有效的唤醒,则处理并继续。
如果不是有效的唤醒,你就回去等待。
在您的情况下,您没有任何数据受到保护,您无法区分虚假唤醒和真实唤醒,并且您的设计不完整。
对于不完整的设计,您看不到重新锁定 mutex 的原因并不奇怪:它已重新锁定,因此您可以安全地检查数据以查看唤醒是否是虚假的。
如果你想知道为什么条件变量是这样设计的,可能是因为这种设计比“可靠”的设计更有效(无论出于何种原因),而且 C++ 没有暴露更高级别的原语,而是更有效地暴露了较低级别原语。
在此之上构建更高级别的抽象并不难,但需要做出设计决策。这是一个建立在std::experimental::optional之上的:
template<class T>
struct data_passer {
std::experimental::optional<T> data;
bool abort_flag = false;
std::mutex guard;
std::condition_variable signal;
void send( T t ) {
{
std::unique_lock<std::mutex> _(guard);
data = std::move(t);
}
signal.notify_one();
}
void abort() {
{
std::unique_lock<std::mutex> _(guard);
abort_flag = true;
}
signal.notify_all();
}
std::experimental::optional<T> get() {
std::unique_lock<std::mutex> _(guard);
signal.wait( _, [this]()->bool{
return data || abort_flag;
});
if (abort_flag) return {};
T retval = std::move(*data);
data = {};
return retval;
}
};
现在,每个send 都可以导致get 在另一端成功。如果出现多个send,则get 仅使用最新的一个。如果设置了abort_flag,则get() 立即返回{};
以上支持多个消费者和生产者。
如何使用上述内容的一个示例是预览状态源(例如,UI 线程)和一个或多个预览渲染器(速度不够快,无法在 UI 线程中运行)。
预览状态将预览状态转储到data_passer<preview_state> willy-nilly。渲染器竞争,其中一个抓住了它。然后他们渲染它,并将它传回(通过任何机制)。
如果预览状态的出现速度快于渲染器消耗它们的速度,则只有最近的状态是感兴趣的,所以较早的状态将被丢弃。但现有预览不会因为出现新状态而中止。
关于比赛条件的以下问题。
如果正在传送的数据是atomic,我们不能没有“发送”端的互斥锁吗?
所以是这样的:
template<class T>
struct data_passer {
std::atomic<std::experimental::optional<T>> data;
std::atomic<bool> abort_flag = false;
std::mutex guard;
std::condition_variable signal;
void send( T t ) {
data = std::move(t); // 1a
signal.notify_one(); // 1b
}
void abort() {
abort_flag = true; // 1a
signal.notify_all(); // 1b
}
std::experimental::optional<T> get() {
std::unique_lock<std::mutex> _(guard); // 2a
signal.wait( _, [this]()->bool{ // 2b
return data.load() || abort_flag.load(); // 2c
});
if (abort_flag.load()) return {};
T retval = std::move(*data.load());
// data = std::experimental::nullopt; // doesn't make sense
return retval;
}
};
上述方法无效。
我们从监听线程开始。它执行步骤 2a,然后等待 (2b)。它在步骤 2c 评估条件,但还没有从 lambda 返回。
广播线程然后执行步骤 1a(设置数据),然后向条件变量发出信号。此时,没有人在等待条件变量(lambda 中的代码不算!)。
监听线程然后完成 lambda,并返回“虚假唤醒”。然后它会阻塞条件变量,并且永远不会注意到数据已发送。
在等待条件变量时使用的std::mutex 必须保护对条件变量“传递”的数据的写入(无论您做什么测试来确定唤醒是否是虚假的)和读取(在 lambda 中) ,或存在“丢失信号”的可能性。 (至少在一个简单的实现中:更复杂的实现可以为“常见情况”创建无锁路径,并且只在仔细检查中使用mutex。这超出了这个问题的范围。)
使用atomic 变量并不能解决这个问题,因为“确定消息是否为虚假”和“在条件变量中重新等待”这两个操作对于消息的“虚假性”而言必须是原子的。