原文:http://www.modernescpp.com/index.php/c-core-guidelines-be-aware-of-the-traps-of-condition-variables
条件变量支持一个很简单的功能,一个线程准备好数据,然后向另外一个线程发送通知,通知另一个线程处理这些数据.为什么有可能会很危险呢?
有这样的一个规则:一个没有条件(condition)的等待有可能错失线程唤醒,或者线程唤醒后没有工作可做.这句话是什么意思呢?条件变量有可能存在两个问题:丢失线程唤醒和虚假唤醒(spurious wakeup),条件变量的一个问题就是他们不占内存.
首先我们先来看这样一段程序:
// conditionVariables.cpp
#include
#include
#include
std::mutex mutex_;
std::condition_variable condVar;
bool dataReady{false};
void waitingForWork(){
std::cout << "Waiting " << std::endl;
std::unique_lock lck(mutex_);
condVar.wait(lck, []{ return dataReady; }); // (4)
std::cout << "Running " << std::endl;
}
void setDataReady(){
{
std::lock_guard lck(mutex_);
dataReady = true;
}
std::cout << "Data prepared" << std::endl;
condVar.notify_one(); // (3)
}
int main(){
std::cout << std::endl;
std::thread t1(waitingForWork); // (1)
std::thread t2(setDataReady); // (2)
t1.join();
t2.join();
std::cout << std::endl;
}
这段程序是如何实现线程间同步的呢?程序有两个线程t1,t2,分别执行waitingForWork和setDataRead.
setDataReady进行通知:使用条件变量condVar,通过condVar.notify()(line 3)进行通知.线程t1拿到锁之后,等待通知condVar.wait(lck, []{ return dataReady; })( line 4).发送者和接受者都需要一个锁,对于发送者来说,使用lock_guard就足够了,因为发送方只需要lock和unlock一次.对于接收者来说,需要使用unique_lock,因为接受者需要经常进行lock和unlock操作.
也许你会奇怪,wati方法为什么需要一个predicate?因为从理论上分析wait方法不需要predicate也可以实现这个功能.下面我们再看一下线程的唤醒丢失和虚假唤醒
唤醒丢失:这个现象是指发送方在接受方进入wait之前就发送了通知,结果就是发送方发送的这个通知丢失,c++标准中把条件变量描述成一个同时的同步机制(simultaneous synchronisation mechanism):条件变量是一种原始的同步机制,可以同时阻塞一个或多个线程.如果不使用predicate,当发送方的通知丢失的时候,接收方会永远等待下去.
虚假唤醒:在接收方未收到通知的时候,接收方线程也有可能会唤醒.
由于存在这两个问题,你必须使用额外的predicate.如果你不相信,我们再看一下条件变量wait方法的工作流程:
在调用wait之前,线程需要拿到锁并加锁.
执行wait,进程首先判断predicate,如果predicate为true,线程继续运行.如果predicate为false,线程unlock锁,进入等待(阻塞)状态.
如果条件变量处于等待状态的时候收到了通知或虚假唤醒,线程会执行一下步骤:
线程解除阻塞,重新lock住锁,判断predicate,如果predicate为true,线程继续工作,如果为false,则unlock锁,线程重新进入等待阻塞态.
如果没有predicate,会发生什么呢?
// conditionVariableWithoutPredicate.cpp
#include
#include
#include
std::mutex mutex_;
std::condition_variable condVar;
void waitingForWork(){
std::cout << "Waiting " << std::endl;
std::unique_lock lck(mutex_);
condVar.wait(lck); // (1)
std::cout << "Running " << std::endl;
}
void setDataReady(){
std::cout << "Data prepared" << std::endl;
condVar.notify_one(); // (2)
}
int main(){
std::cout << std::endl;
std::thread t1(waitingForWork);
std::thread t2(setDataReady);
t1.join();
t2.join();
std::cout << std::endl;
}
现在,对于wait的调用不再使用predicate,这种同步机制看起来很简单.但是不幸的是,这个程序会存在一个 race condition,会引发死锁.
发送方在line(1)发送通知,发送方在接受方wait之前就发送了通知,接收方就永久阻塞了.
也许你已经注意到了,dataReady变量只是一个布尔变量,我们可不可以把它变成一个原子的布尔变量,这样就可以不加锁了呢?
// conditionVariableAtomic.cpp
#include
#include
#include
#include
std::mutex mutex_;
std::condition_variable condVar;
std::atomic dataReady{false};
void waitingForWork(){
std::cout << "Waiting " << std::endl;
std::unique_lock lck(mutex_);
condVar.wait(lck, []{ return dataReady.load(); }); // (1)
std::cout << "Running " << std::endl;
}
void setDataReady(){
dataReady = true;
std::cout << "Data prepared" << std::endl;
condVar.notify_one();
}
int main(){
std::cout << std::endl;
std::thread t1(waitingForWork);
std::thread t2(setDataReady);
t1.join();
t2.join();
std::cout << std::endl;
}
这个程序很直观,但是这个程序,线程间依然有race condition并且会导致死锁.因为wait操作比看起来的还要复杂一些.wait操作等价于:
std::unique_lock lck(mutex_);
while ( ![]{ return dataReady.load(); }() {
// time window (1)
condVar.wait(lck);
}
即使你把dataReady设置成原子变量,但是也必须加锁操作,如果不加锁的话可能无法实现线程同步.我们假设所有变量的操作是原子的并且没有锁保护.
我们假设接收方在执行wait但是线程没有阻塞,即接收方执行line 1的代码段,这样的话发送方发送的通知就丢失了,接下来接受方会永久进入阻塞状态.但是如果加锁进行保护的话,这种情况就不会再发生,当加锁保护的时候,只有接收方处于等待态的时候,发送方才能发送通知.
这是不是一个鬼故事?那么有没有其他的可能来简化我们的程序呢?有的,但不是使用条件变量,你可以使用一对promise和future(a promise and future pair the make the job done).