为什么 'wait with predicate' 解决了条件变量的 'lost wakeup'?
Why 'wait with predicate' solves the 'lost wakeup' for condition variable?
我试图了解在条件变量的情况下虚假唤醒与丢失唤醒之间的区别。以下是我试过的一小段代码。我知道 'consumer' 在这种情况下可能会在没有任何通知的情况下醒来,因此等待需要检查谓词。
但是 wait with predicate 如何解决 'lost wakeup' 的问题呢?正如您在下面的代码中看到的; 'wait' 5 秒内未被调用,我原以为它会错过前几个通知;但是有了 predate,它就不会错过任何东西。这些通知是否保存以备将来等待?
#include <iostream>
#include <deque>
#include <condition_variable>
#include <thread>
std::deque<int> q;
std::mutex m;
std::condition_variable cv;
void dump_q()
{
for (auto x: q) {
std::cout << x << std::endl;
}
}
void producer()
{
for(int i = 0; i < 10; i++) {
std::unique_lock<std::mutex> locker(m);
q.push_back(i);
std::cout << "produced: " << i << std::endl;
cv.notify_one();
std::this_thread::sleep_for(std::chrono::seconds(1));
locker.unlock();
}
}
void consumer()
{
while (true) {
int data = 0;
std::this_thread::sleep_for(std::chrono::seconds(5)); // <- should miss first 5 notications?
std::unique_lock<std::mutex> locker(m);
cv.wait(locker);
//cv.wait(locker, [](){return !q.empty();}); // <- this fixes both spurious and lost wakeups
data = q.front();
q.pop_front();
std::cout << "--> consumed: " << data << std::endl;
locker.unlock();
}
}
int main(int argc, char *argv[])
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
这是防止丢失唤醒的原子"unlock and wait"操作。丢失的唤醒是这样发生的:
- 我们获取保护数据的锁。
- 我们检查是否需要等待,我们发现需要等待。
- 我们需要释放锁,否则其他线程无法访问数据。
- 我们等待唤醒。
您可以在此处查看丢失唤醒的风险。在第 3 步和第 4 步之间,另一个线程可以获取锁并发送唤醒。我们已经释放了锁,所以另一个线程可以执行此操作,但我们还没有等待,所以我们不会收到信号。
只要第2步在锁的保护下完成并且第3步和第4步是原子的,就不存在丢失唤醒的风险。在修改数据之前不能发送唤醒,而在另一个线程获取锁之前不能完成。由于 3 和 4 是原子的,任何将锁视为已解锁的线程也必然会看到我们在等待。
这个原子 "unlock and wait" 是条件变量的主要目的,也是它们必须始终与互斥锁和谓词相关联的原因。
In code above, consumer is not waiting for first few notifications because it is sleeping. Is it not missing notify in this case? Is this case not similar to race condition between #3 and #4?
没有。不可能发生。
不是等待的消费者持有锁,就是没有。如果没有等待的消费者持有锁,它就不会错过任何东西。谓词在持有锁时不能更改。
如果消费者没有持有锁,那么错过什么都没有关系。当它检查是否应该在步骤 2 中锁定时,如果它错过了什么,它必然会在步骤 2 中看到它并且它会看到它不需要等待,所以它不会等待它错过的唤醒。
所以如果谓词是这样的线程不需要等待,线程将不会等待,因为它检查谓词。在步骤 1 之前没有机会错过唤醒。
唯一需要实际唤醒的时间是线程进入休眠状态。原子解锁和睡眠确保一个线程只有在它持有锁并且它需要等待的事情还没有发生时才能决定进入睡眠状态。
我试图了解在条件变量的情况下虚假唤醒与丢失唤醒之间的区别。以下是我试过的一小段代码。我知道 'consumer' 在这种情况下可能会在没有任何通知的情况下醒来,因此等待需要检查谓词。
但是 wait with predicate 如何解决 'lost wakeup' 的问题呢?正如您在下面的代码中看到的; 'wait' 5 秒内未被调用,我原以为它会错过前几个通知;但是有了 predate,它就不会错过任何东西。这些通知是否保存以备将来等待?
#include <iostream>
#include <deque>
#include <condition_variable>
#include <thread>
std::deque<int> q;
std::mutex m;
std::condition_variable cv;
void dump_q()
{
for (auto x: q) {
std::cout << x << std::endl;
}
}
void producer()
{
for(int i = 0; i < 10; i++) {
std::unique_lock<std::mutex> locker(m);
q.push_back(i);
std::cout << "produced: " << i << std::endl;
cv.notify_one();
std::this_thread::sleep_for(std::chrono::seconds(1));
locker.unlock();
}
}
void consumer()
{
while (true) {
int data = 0;
std::this_thread::sleep_for(std::chrono::seconds(5)); // <- should miss first 5 notications?
std::unique_lock<std::mutex> locker(m);
cv.wait(locker);
//cv.wait(locker, [](){return !q.empty();}); // <- this fixes both spurious and lost wakeups
data = q.front();
q.pop_front();
std::cout << "--> consumed: " << data << std::endl;
locker.unlock();
}
}
int main(int argc, char *argv[])
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
这是防止丢失唤醒的原子"unlock and wait"操作。丢失的唤醒是这样发生的:
- 我们获取保护数据的锁。
- 我们检查是否需要等待,我们发现需要等待。
- 我们需要释放锁,否则其他线程无法访问数据。
- 我们等待唤醒。
您可以在此处查看丢失唤醒的风险。在第 3 步和第 4 步之间,另一个线程可以获取锁并发送唤醒。我们已经释放了锁,所以另一个线程可以执行此操作,但我们还没有等待,所以我们不会收到信号。
只要第2步在锁的保护下完成并且第3步和第4步是原子的,就不存在丢失唤醒的风险。在修改数据之前不能发送唤醒,而在另一个线程获取锁之前不能完成。由于 3 和 4 是原子的,任何将锁视为已解锁的线程也必然会看到我们在等待。
这个原子 "unlock and wait" 是条件变量的主要目的,也是它们必须始终与互斥锁和谓词相关联的原因。
In code above, consumer is not waiting for first few notifications because it is sleeping. Is it not missing notify in this case? Is this case not similar to race condition between #3 and #4?
没有。不可能发生。
不是等待的消费者持有锁,就是没有。如果没有等待的消费者持有锁,它就不会错过任何东西。谓词在持有锁时不能更改。
如果消费者没有持有锁,那么错过什么都没有关系。当它检查是否应该在步骤 2 中锁定时,如果它错过了什么,它必然会在步骤 2 中看到它并且它会看到它不需要等待,所以它不会等待它错过的唤醒。
所以如果谓词是这样的线程不需要等待,线程将不会等待,因为它检查谓词。在步骤 1 之前没有机会错过唤醒。
唯一需要实际唤醒的时间是线程进入休眠状态。原子解锁和睡眠确保一个线程只有在它持有锁并且它需要等待的事情还没有发生时才能决定进入睡眠状态。