条件变量(二)
条件变量是用来等待某个条件是否成立的等待原语。主要有两个函数:
1、 pthread_cond_signal(&cond) :用来发送信号,唤醒正在wait()ing的线程。要搞清楚这里所谓唤醒,要知道其实线程中有两个队列cond_wait和mutex_lock。signal的唤醒只是将cond_wait中的一个线程放入mutex_lock中,而不是回到正常工作状态(所以性能损耗可忽略)。代码中一般在调用signal后,紧接着会解锁,此时mutex_lock队列中的阻塞线程获得互斥锁,上锁。
2、 pthread_cond_wait() :用来等待信号。一般(个人觉得是必须)用while对某个条件进行判断,条件不满足时进入调用pthread_cond_wait()函数。调用wait时先后做了这几个动作:
解锁-->挂起等待(进入cond_wait,等待signal唤醒)-->被signal唤醒,进入(cond_mutex队列)--重新加锁-
然后进入下一次while循环,判断条件是否还满足。
循环判断的原因如下:两个线程同时调用signal,唤醒两个cond_wait队列中的线程,但是他们存在race condition,两个线程要竞争同一个互斥锁:比如线程1、2竞争,但是线程1竞争到了互斥锁,那么线程2有阻塞了;虽然已经从cond_wait中出来了(并不是从pthread_cond_wait返回);等到线程1释放锁,线程2持有了锁,此时必须循环再检查条件,因为条件已经被线程2修改了。这就是为什么要用while而不是if的原因。避免wait从队列中出来、条件被别的线程修改了。如果是if就造成了虚假唤醒(suprious wake)。为避免这点,必须醒来后再次判断睡眠条件。
下面是一段简单的多线程代码:
1 pthread_mutex_t m=PTHREAD_MUTEX_INITIALIZER; 2 pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 3 bool signaled =false; 4 5 void waiter() 6 { 7 pthread_mutex_lock(&m); 8 while(!signaled) //一定要是while,而不是if 9 pthread_cond_wait(&cond, &m); 10 11 dosomething(x,y,...); 12 pthread_mutex_unlock(&m); 13 } 14 15 void signal() 16 { 17 pthread_mutex_lock(&m); 18 signaled = true; //先改变条件,再调用signal 19 pthread_cond_signal(&cond); 20 pthread_mutex_unlock(&m); 21 }
至于为什么在被唤醒之后还要再次进行条件判断(即为什么要使用while循环来判断条件),是因为可能有“惊群效应”。有人觉得此处既然是被唤醒的,肯定是满足条件了,其实不然。如果是多个线程都在等待这个条件,而同时只能有一个线程进行处理,此时就必须要再次条件判断,以使只有一个线程进入临界区处理。
至于先改变条件值,再调用pthread_cond_signal是因为如果先调用pthread_cond_signal唤醒pthread_cond_wait线程,而此时还没来得及改变signaled,此时wait线程再次while判断signaled的值,发现条件不满足,再次陷入pthread_cond_wait()。这就叫做事件丢失。我在github上问过陈硕大神https://github.com/chenshuo/recipes/issues/18,问他通知和修改是不是写反了,他说持有锁的情况下这么写是正确的,不会导致事件丢失。代码如下:
1 void broadcast() 2 { 3 pthread_mutex_lock(&mutex_); 4 pthread_cond_broadcast(&cond_); 5 signaled_ = true; 6 pthread_mutex_unlock(&mutex_); 7 }
从上文可以看出:
1,pthread_cond_signal在多处理器上可能同时唤醒多个线程,当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续 wait,while循环的意义就体现在这里了,而且规范要求pthread_cond_signal至少唤醒一个pthread_cond_wait上的线程,其实有些实现为了简单在单处理器上也会唤醒多个线程.
2,某些应用,如线程池,pthread_cond_broadcast唤醒全部线程,但我们通常只需要一部分线程去做执行任务,所以其它的线程需要继续wait.所以强烈推荐此处使用while循环.
其实说白了很简单,就是pthread_cond_signal()也可能唤醒多个线程,而如果你同时只允许一个线程访问的话,就必须要使用while来进行条件判断,以保证临界区内只有一个线程在处理。