对于互斥量和条件变量的使用,很多书上早已给出了标准的使用方式,这里以SSDB中的Queue为例:
-
template <class T>
-
// 通知方
-
int Queue<T>::push(const T item){
-
// 1. 加锁 lock
-
if(pthread_mutex_lock(&mutex) != 0){
-
return -1;
-
}
-
// 2. 修改条件
-
{
-
items.push(item);
-
}
-
// 3. 解锁 unlock
-
pthread_mutex_unlock(&mutex);
-
// 4. signal通知
-
pthread_cond_signal(&cond);
-
return 1;
-
}
-
-
template <class T>
-
// 等待方
-
int Queue<T>::pop(T *data){
-
// 1. 加锁 lock
-
if(pthread_mutex_lock(&mutex) != 0){
-
return -1;
-
}
-
{
-
// 2. 循环检查条件
-
while(items.empty()){
-
// 3. wait 等待条件变化
-
if(pthread_cond_wait(&cond, &mutex) != 0){
-
return -1;
-
}
-
}
-
*data = items.front();
-
items.pop();
-
}
-
// 4. 解锁 unlock
-
if(pthread_mutex_unlock(&mutex) != 0){
-
return -1;
-
}
-
return 1;
-
}
总结起来,等待方和通知方大体上就是上面的四步操作,对此也没有多想,直到有一天,Muduo作者陈硕发了一条微博 。
初看起来,没觉得红框处有什么问题,这样封装相比起标准方法就是没有用循环检查条件是否满足,说不定这段代码的作者想在wait函数的外层用循环检查了,但是问题就在这里,即使在wait的外层用循环去检查也是错的,
因为在这个wait函数的结尾已经将锁释放了,在不持有锁的情况下去检查条件是否满足这是完全错误的。因此这个wait函数是没法用的,它就是错误的。这里补充一点儿pthread_cond_wait的知识:
pthread_cond_wait函数会原子的将调用线程放到等待队列中并释放互斥量,这样其他的线程就可以对互斥量加锁修改共享数据,
然后调用pthread_cond_signal / phthread_cond_broadcast并解锁(两者的顺序无关紧要),这时pthread_cond_wait函数会获取锁并把线程从等待队列中拉出来,然后pthread_cond_wait函数返回。其实从道理上来说,对于共享数据,读写访问都是要加锁的,上面的wait函数释放了锁是不可能正确的检查条件的。
这应该就是原Po说的意思,微博发出后,大家讨论的很热烈,很快大家就提出了自己的疑问:
1. 这段代码是在模拟Windows下的Event,wait函数返回就说明事件被触发了。
很可惜,这样也是错的,因为事件机制隐含的条件是wait和signal的执行有先后顺序,一定是等待方先wait了之后,通知方才能signal,但是上面的代码没有采取任何措施保证wait先于signal,如果signal先执行,再来wait,那事件就永远不会被捕获到了,而调用wait的线程会一直等待。
因此,正确的模拟Event的代码应该是下面这样:利用一个signaled_来判断是否有事件发生,即使先调用了signal,wait函数也不会等待
-
// Version 7: broadcast to wakeup multiple waiting threads
-
// Probably the best version among above.
-
class Waiter7 : public Waiter
-
{
-
public:
-
void wait() override
-
{
-
pthread_mutex_lock(&mutex_);
-
while (!signaled_)
-
{
-
pthread_cond_wait(&cond_, &mutex_);
-
}
-
pthread_mutex_unlock(&mutex_);
-
}
-
-
void signal() override // Sorry, bad name in base class, poor OOP
-
{
-
broadcast();
-
}
-
-
void broadcast()
-
{
-
pthread_mutex_lock(&mutex_);
-
pthread_cond_broadcast(&cond_);
-
signaled_ = true;
-
pthread_mutex_unlock(&mutex_);
-
}
-
-
private:
-
bool signaled_ = false;
-
};
下面这两句话取自陈硕的文章: http://www.cppblog.com/Solstice/archive/2013/09/09/203094.html
总结:使用条件变量,调用 signal() 的时候无法知道是否已经有线程等待在 wait() 上。因此一般总是要先修改“条件”,使其为 true,再调用 signal();这样 wait 线程先检查“条件”,只有当条件不成立时才去 wait(),避免了丢事件的可能。换言之,通过使用“条件”,将边沿触发(edge trigger)改为电平触发(level trigger)。这里“修改条件”和“检查条件”都必须在 mutex 保护下进行,而且这个 mutex 必须用于配合 wait()。
style="font-family:Verdana, Geneva, Arial, Helvetica, sans-serif;font-size:16px;line-height:normal;white-space:normal;"> 这篇帖子里对 spurious wakeup 的解释是错的,spurious wakeup 指的是一次 signal() 调用唤醒两个或以上 wait()ing 的线程,或者没有调用 signal() 却有线程从 wait() 返回。manpage 里对 Pthreads 系列函数的介绍非常到位,值得细读。
2. pthread_cond_signal / pthread_cond_broadcast 和 pthread_mutex_unlock的顺序问题:
-
template <class T>
-
int Queue<T>::push(const T item){
-
if(pthread_mutex_lock(&mutex) != 0){
-
return -1;
-
}
-
{
-
items.push(item);
-
}
-
// signal是否需要放到unlock之前 ??
-
pthread_mutex_unlock(&mutex);
-
pthread_cond_signal(&cond);
-
-
return 1;
-
}
实际上,这两者的顺序是没有要求的,唯一有要求的就是修改共享条件,状态必须在加锁情况下,至于 pthread_cond_signal / pthread_cond_broadcast 是否需要加锁,完全没要求,在TLPI(The Linux Programming Interface)中,有详细的解释。
阅读(1593) | 评论(0) | 转发(0) |