一般来说,处理虚假唤醒的正确方法是什么?
What's the correct way to deal with spurious wakeups, in general?
在下面的选项中,有没有正确的方法来处理使用条件变量时的虚假唤醒?
1) 使用布尔值
将 wait(unique_lock_ul)
放入无限 while
循环
unique_lock<mutex> ul(m);
while(!full)
cv.wait(ul);
2) 同 if
unique_lock<mutex> ul(m);
if(!full)
cv.wait(ul);
3) 在 wait()
中放置一个条件,例如使用 lambda 函数
unique_lock<mutex> ul(m);
cv.wait(ul, [&](){return !full;});
如果 none 是正确的,如何轻松处理虚假唤醒?
我对 C++ 中的条件变量相当陌生,我不确定我阅读的某些代码是否处理虚假唤醒的情况。
简短的回答是,您的代码可能正确也可能错误;您没有确切说明 full
是如何被操纵的。
C++ 代码的个别位永远不是线程安全的。线程安全是关系属性的代码;如果两位代码永远不会导致竞争条件,那么它们可以是线程安全的。
但是一点代码永远都不是线程安全的;说某物是线程安全的就像说某物是“相同高度”一样。
“monkey see monkey do”条件变量模式是这样的:
template<class T>
class cv_bundle {
std::mutex m;
T payload;
std::condition_variable cv;
public:
explicit cv_bundle( T in ):payload(std::move(in)) {}
template<class Test, class Extract>
auto wait( Test&& test, Extract&& extract ) {
std::unique_lock<std::mutex> l(m);
cv.wait( l, [&]{ return test(payload); } );
return extract(payload);
}
template<class Setter>
void load( Setter&& setter, bool only_one = true ) {
std::unique_lock<std::mutex> l(m);
bool is_set = setter( payload );
if (!is_set) return; // nothing to notify
if (only_one)
cv.notify_one();
else
cv.notify_all();
}
};
test
需要一个 T& payload
并且 return 如果有东西要消耗(即唤醒不是虚假的)则为真。
extract
接受 T& payload
和 return 任何你想从中得到的信息。它通常应该重置有效负载。
setter
以 test
将 return true
的方式修改 T& payload
。如果这样做,它 returns true
。如果它选择不这样做,它 returns false
.
所有 3 个都在互斥锁定访问 T payload
中调用。
现在,您可以生成变体,但很难做到正确。例如,不要假设原子负载意味着您不必锁定互斥量。
虽然我将这 3 个东西捆绑在一起,但您可以将单个互斥锁用于一堆条件变量,或者将互斥锁用于不仅仅是条件变量。有效载荷可以是一个布尔值、一个计数器、一个数据向量,或者更奇怪的东西;通常,它必须始终受到互斥锁的保护。如果它是原子的,在被修改的值和通知之间的开放时间间隔中的某个时间点,互斥体必须被锁定,否则你有丢失通知的风险。
手动循环控制而不是传入lambda是一个修改,但是描述什么样的手动循环是合法的,哪些是竞争条件是一个复杂的问题。
实际上,除非我有非常充分的理由,否则我避免离开这种猴子看猴子做的“货物崇拜”风格的条件变量使用。然后我被迫阅读 C++ 内存和线程模型,这让我很不高兴,这意味着我的代码很可能不正确。
请注意,如果传入的任何 lambda 表达式返回并回调 cv_bundle
,我显示的代码将不再有效。
1 或 3 种方法都可以处理虚假唤醒(假设 full
修改受同一个互斥体保护)除了谓词条件错误,它应该是:
unique_lock<mutex> ul(m);
cv.wait(ul, [&](){return full;});
使此代码等于变体 1。
变体 2 并不好,因为不会重新检查虚假唤醒等待条件,这与其他 2 种情况不同。
在下面的选项中,有没有正确的方法来处理使用条件变量时的虚假唤醒?
1) 使用布尔值
将wait(unique_lock_ul)
放入无限 while
循环
unique_lock<mutex> ul(m);
while(!full)
cv.wait(ul);
2) 同 if
unique_lock<mutex> ul(m);
if(!full)
cv.wait(ul);
3) 在 wait()
中放置一个条件,例如使用 lambda 函数
unique_lock<mutex> ul(m);
cv.wait(ul, [&](){return !full;});
如果 none 是正确的,如何轻松处理虚假唤醒?
我对 C++ 中的条件变量相当陌生,我不确定我阅读的某些代码是否处理虚假唤醒的情况。
简短的回答是,您的代码可能正确也可能错误;您没有确切说明 full
是如何被操纵的。
C++ 代码的个别位永远不是线程安全的。线程安全是关系属性的代码;如果两位代码永远不会导致竞争条件,那么它们可以是线程安全的。
但是一点代码永远都不是线程安全的;说某物是线程安全的就像说某物是“相同高度”一样。
“monkey see monkey do”条件变量模式是这样的:
template<class T>
class cv_bundle {
std::mutex m;
T payload;
std::condition_variable cv;
public:
explicit cv_bundle( T in ):payload(std::move(in)) {}
template<class Test, class Extract>
auto wait( Test&& test, Extract&& extract ) {
std::unique_lock<std::mutex> l(m);
cv.wait( l, [&]{ return test(payload); } );
return extract(payload);
}
template<class Setter>
void load( Setter&& setter, bool only_one = true ) {
std::unique_lock<std::mutex> l(m);
bool is_set = setter( payload );
if (!is_set) return; // nothing to notify
if (only_one)
cv.notify_one();
else
cv.notify_all();
}
};
test
需要一个 T& payload
并且 return 如果有东西要消耗(即唤醒不是虚假的)则为真。
extract
接受 T& payload
和 return 任何你想从中得到的信息。它通常应该重置有效负载。
setter
以 test
将 return true
的方式修改 T& payload
。如果这样做,它 returns true
。如果它选择不这样做,它 returns false
.
所有 3 个都在互斥锁定访问 T payload
中调用。
现在,您可以生成变体,但很难做到正确。例如,不要假设原子负载意味着您不必锁定互斥量。
虽然我将这 3 个东西捆绑在一起,但您可以将单个互斥锁用于一堆条件变量,或者将互斥锁用于不仅仅是条件变量。有效载荷可以是一个布尔值、一个计数器、一个数据向量,或者更奇怪的东西;通常,它必须始终受到互斥锁的保护。如果它是原子的,在被修改的值和通知之间的开放时间间隔中的某个时间点,互斥体必须被锁定,否则你有丢失通知的风险。
手动循环控制而不是传入lambda是一个修改,但是描述什么样的手动循环是合法的,哪些是竞争条件是一个复杂的问题。
实际上,除非我有非常充分的理由,否则我避免离开这种猴子看猴子做的“货物崇拜”风格的条件变量使用。然后我被迫阅读 C++ 内存和线程模型,这让我很不高兴,这意味着我的代码很可能不正确。
请注意,如果传入的任何 lambda 表达式返回并回调 cv_bundle
,我显示的代码将不再有效。
1 或 3 种方法都可以处理虚假唤醒(假设 full
修改受同一个互斥体保护)除了谓词条件错误,它应该是:
unique_lock<mutex> ul(m);
cv.wait(ul, [&](){return full;});
使此代码等于变体 1。
变体 2 并不好,因为不会重新检查虚假唤醒等待条件,这与其他 2 种情况不同。