为什么 condition_variable 没有不重新锁定互斥锁的等待函数
Why is there no wait function for condition_variable which does not relock the mutex
考虑以下示例。
std::mutex mtx;
std::condition_variable cv;
void f()
{
{
std::unique_lock<std::mutex> lock( mtx );
cv.wait( lock ); // 1
}
std::cout << "f()\n";
}
void g()
{
std::this_thread::sleep_for( 1s );
cv.notify_one();
}
int main()
{
std::thread t1{ f };
std::thread t2{ g };
t2.join();
t1.join();
}
g()
"knows" f()
正在等待我想讨论的场景。
根据 cppreference.com,g()
在调用 notify_one
之前不需要锁定互斥锁。现在,在标记为“1”的行中,cv
将释放互斥量并在发送通知后重新锁定它。 lock
的析构函数在那之后立即再次释放它。这似乎是多余的,尤其是因为锁定很昂贵。 (我知道在某些情况下需要锁定互斥量。但这里不是这种情况。)
为什么 condition_variable
没有函数“wait_nolock
”一旦通知到达就不会重新锁定互斥体。如果答案是 pthreads 不提供这样的功能:为什么不能扩展 pthreads 来提供它?是否有实现所需行为的替代方案?
您误解了代码的作用。
您在行 // 1
上的代码完全没有阻塞。 condition_variables
可以(并且将会!)进行虚假唤醒——它们可以完全无缘无故地唤醒。
您有责任检查唤醒是否虚假。
正确使用 condition_variable
需要三件事:
- 一个
condition_variable
- 一个
mutex
mutex
保护的一些数据
互斥锁保护的数据被修改(在mutex
下)。然后(mutex
可能脱离),通知 condition_variable
。
在另一端,您锁定 mutex
,然后等待条件变量。当你醒来时,你的 mutex
被重新锁定,你可以通过查看 mutex
保护的数据来测试唤醒是否虚假。如果是有效唤醒,则处理并继续。
如果不是有效的唤醒,您返回等待。
在你的情况下,你没有任何数据保护,你无法区分虚假唤醒和真实唤醒,你的设计不完整。
对于不完整的设计,您看不到重新锁定 mutex
的原因不足为奇:它已重新锁定,因此您可以安全地检查数据以查看唤醒是否虚假。
如果你想知道为什么这样设计条件变量,可能是因为这种设计比 "reliable" 更有效(无论出于何种原因),而不是公开更高级别的原语,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()
立即 returns {}
;
以上支持多个消费者和生产者。
预览状态源(例如 UI 线程)和一个或多个预览渲染器(速度不够快 运行 在 UI 线程中)。
预览状态将预览状态转储到 data_passer<preview_state>
中。渲染器竞争,其中之一抓住了它。然后他们渲染它,并将它传回(通过任何机制)。
如果预览状态出现的速度快于渲染器消耗它们的速度,则只有最近的状态才有意义,所以较早的状态会被丢弃。但现有预览不会因为出现新状态而中止。
下面询问的有关竞争条件的问题。
如果正在通信的数据是atomic
,我们不能没有"send"端的互斥量吗?
所以像这样:
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 return。
广播线程然后执行步骤 1a(设置数据),然后向条件变量发送信号。此时,没有人在等待条件变量(lambda 中的代码不算!)。
侦听线程然后完成 lambda,并且 returns "spurious wakeup"。然后它会阻塞条件变量,并且永远不会注意到数据已发送。
等待条件变量时使用的 std::mutex
必须保护条件变量对数据 "passed" 的写入(无论您做什么测试以确定唤醒是否虚假),以及读取(在 lambda 中),或者存在 "lost signals" 的可能性。 (至少在一个简单的实现中:更复杂的实现可以为 "common cases" 创建无锁路径,并且只在双重检查中使用 mutex
。这超出了这个问题的范围。)
使用atomic
变量并不能解决这个问题,因为"determine if the message was spurious"和"rewait in the condition variable"这两个操作对于消息的"spuriousness"必须是原子的.
考虑以下示例。
std::mutex mtx;
std::condition_variable cv;
void f()
{
{
std::unique_lock<std::mutex> lock( mtx );
cv.wait( lock ); // 1
}
std::cout << "f()\n";
}
void g()
{
std::this_thread::sleep_for( 1s );
cv.notify_one();
}
int main()
{
std::thread t1{ f };
std::thread t2{ g };
t2.join();
t1.join();
}
g()
"knows" f()
正在等待我想讨论的场景。
根据 cppreference.com,g()
在调用 notify_one
之前不需要锁定互斥锁。现在,在标记为“1”的行中,cv
将释放互斥量并在发送通知后重新锁定它。 lock
的析构函数在那之后立即再次释放它。这似乎是多余的,尤其是因为锁定很昂贵。 (我知道在某些情况下需要锁定互斥量。但这里不是这种情况。)
为什么 condition_variable
没有函数“wait_nolock
”一旦通知到达就不会重新锁定互斥体。如果答案是 pthreads 不提供这样的功能:为什么不能扩展 pthreads 来提供它?是否有实现所需行为的替代方案?
您误解了代码的作用。
您在行 // 1
上的代码完全没有阻塞。 condition_variables
可以(并且将会!)进行虚假唤醒——它们可以完全无缘无故地唤醒。
您有责任检查唤醒是否虚假。
正确使用 condition_variable
需要三件事:
- 一个
condition_variable
- 一个
mutex
mutex
保护的一些数据
互斥锁保护的数据被修改(在mutex
下)。然后(mutex
可能脱离),通知 condition_variable
。
在另一端,您锁定 mutex
,然后等待条件变量。当你醒来时,你的 mutex
被重新锁定,你可以通过查看 mutex
保护的数据来测试唤醒是否虚假。如果是有效唤醒,则处理并继续。
如果不是有效的唤醒,您返回等待。
在你的情况下,你没有任何数据保护,你无法区分虚假唤醒和真实唤醒,你的设计不完整。
对于不完整的设计,您看不到重新锁定 mutex
的原因不足为奇:它已重新锁定,因此您可以安全地检查数据以查看唤醒是否虚假。
如果你想知道为什么这样设计条件变量,可能是因为这种设计比 "reliable" 更有效(无论出于何种原因),而不是公开更高级别的原语,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()
立即 returns {}
;
以上支持多个消费者和生产者。
预览状态源(例如 UI 线程)和一个或多个预览渲染器(速度不够快 运行 在 UI 线程中)。
预览状态将预览状态转储到 data_passer<preview_state>
中。渲染器竞争,其中之一抓住了它。然后他们渲染它,并将它传回(通过任何机制)。
如果预览状态出现的速度快于渲染器消耗它们的速度,则只有最近的状态才有意义,所以较早的状态会被丢弃。但现有预览不会因为出现新状态而中止。
下面询问的有关竞争条件的问题。
如果正在通信的数据是atomic
,我们不能没有"send"端的互斥量吗?
所以像这样:
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 return。
广播线程然后执行步骤 1a(设置数据),然后向条件变量发送信号。此时,没有人在等待条件变量(lambda 中的代码不算!)。
侦听线程然后完成 lambda,并且 returns "spurious wakeup"。然后它会阻塞条件变量,并且永远不会注意到数据已发送。
等待条件变量时使用的 std::mutex
必须保护条件变量对数据 "passed" 的写入(无论您做什么测试以确定唤醒是否虚假),以及读取(在 lambda 中),或者存在 "lost signals" 的可能性。 (至少在一个简单的实现中:更复杂的实现可以为 "common cases" 创建无锁路径,并且只在双重检查中使用 mutex
。这超出了这个问题的范围。)
使用atomic
变量并不能解决这个问题,因为"determine if the message was spurious"和"rewait in the condition variable"这两个操作对于消息的"spuriousness"必须是原子的.