🔎【Java 源码探索】深入浅出的分析 Mutex 底层源码
并行编程之条件变量(posix condition variables)
在整理 Java LockSupport.park()的东东,看到了个"Spurious wakeup",重新梳理下。
一个简单的消息生产者和消费者的代码,它们之间用 condition 同步。
这个代码最容易让人搞混的是 process_msg 函数里的 pthread_mutex_lock 和 pthread_mutex_unlock 是一对函数调用,前面加锁,后面解锁。的确,是加锁解锁,但是它们两不是一对的。它们的另一半在 pthread_cond_wait 函数里。
pthread_cond_wait 函数可以认为它做了三件事:
把自身线程放到 condition 的等待队列里,把 mutex 解锁;
等待被唤醒(当其它线程调用 pthread_cond_signal 或者 pthread_cond_broadcast 时);
被唤醒之后,对 mutex 加锁,再返回。
mutex 和 condition 实际上是绑定在一起的,一个 condition 只能对应一个 mutex。
在 Java 的代码里,Condition 对象只能通过 lock.newCondition()的函数来获取。
Spurious wakeup
所谓的 spurious wakeup,指的是一个线程调用 pthread_cond_signal(),却有可能不止一个线程被唤醒。
假定有三个线程,线程 A 正在执行 pthread_cond_wait,线程 B 正在执行 pthread_cond_signal,线程 C 正准备执行 pthread_cond_wait 函数。
线程 A 执行了第 1,2 步,这时它释放了 mutex,然后线程 B 拿到了这个 mutext,并且 pthread_cond_signal 函数时执行并返回了。
于是线程 B 就是一个所谓的“spurious wakeup”。
/build/buildd/eglibc-2.19/nptl/pthread_cond_wait.c/build/buildd/eglibc-2.19/nptl/pthread_cond_signal.c
wait morphing 优化
从而会有一个叫“wait morphing”优化,就是如果线程被唤醒但是不能获取到 mutex,则线程被转移(morphing)到 mutex 的等待队列里。
The pthread_cond_broadcast() or pthread_cond_signal() functions may be called by a thread whether or not it currently owns the mutex that threads calling pthread_cond_wait() or pthread_cond_timedwait() have associated with the condition variable during their waits; however, if predictable scheduling behavior is required, then that mutex shall be locked by the thread calling pthread_cond_broadcast() or pthread_cond_signal().
是先调用 pthread_mutex_unlock,再调用 pthread_cond_signal。
有的地方给出的是先调用 pthread_cond_signal,再调用 pthread_mutex_unlock:
先 unlock 再 signal,这有个好处,就是调用 enqueue_msg 的线程可以再次参与 mutex 的竞争中,这样意味着可以连续放入多个消息,这个可能会提高效率。类似 Java 里 ReentrantLock 的非公平模式。
先 signal 再 unlock,有可能会出现一种情况是被 signal 唤醒的线程会因为不能马上拿到 mutex(还没被释放),从而会再次休眠,这样影响了效率。
可见在调用 signal 之前,可以不持有 mutex,除非是“predictable scheduling”,可预测的调度行为。这种可能是实时系统才有这种严格的要求。
为什么要用 while 循环来判断条件是否成立?
而不用 if 来判断?
一个原因是 spurious wakeup,但即使没有 spurious wakeup,也是要用 While 来判断的。
线程 A,线程 B 在 pthread_cond_wait 函数中等待,然后线程 C 把消息放到队列里,再调用 pthread_cond_broadcast,然后线程 A 先获取到 mutex,处理完消息完后,这时 workq 就变成 NULL 了。
线程 B 才获取到 mutex,那么这时实际上是没有资源供线程 B 使用的。所以从 pthread_cond_wait 函数返回之后,还是要判断条件是否成功,如果成立,再进行处理。
pthread_cond_signal 和 pthread_cond_broadcast
认为调用 pthread_cond_broadcast 来唤醒所有的线程是比较好的写法。
但是我认为 pthread_cond_signal 和 pthread_cond_broadcast 是两个不同东东,不能简单合并在同一个函数调用。
只唤醒一个效率和唤醒全部等待线程的效率显然不能等同。典型的 condition 是用 CLH 或者 MCS 来实现的,要通知所有的线程,则要历遍链表,显然效率降低。
mutex,condition 是不是公平(fair)的?
输出结果是:
连续调用 pthread_cond_signal,会唤醒多少次/多少个线程?比如线程 a,b 在调用 pthread_cond_wait 之后等待,然后线程 c, d 同时调用 pthread_cond_signal,那么 a, b 线程是否都能被唤醒?
会不会出现 c, d, a 这种调用顺序,然后 b 一直在等待,然后死锁了?
The pthread_cond_signal() function shall unblock at least one of the threads that are blocked on the specified condition variable cond (if any threads are blocked on cond).
因此,如果有线程已经在调用 pthread_cond_wait 等待的情况下,pthread_cond_signal 调用至少会唤醒等待中的一个线程。
所以不会出现上面的线程 b 一直等待的情况。
版权声明: 本文为 InfoQ 作者【李浩宇/Alex】的原创文章。
原文链接:【http://xie.infoq.cn/article/980fc5180940aa623a6681674】。文章转载请联系作者。
评论