并行编程条件变量(posix condition variables)
在整理Java LockSupport.park()东方的,我看到了"Spurious wakeup",通过重新梳理。
首先,可以在《UNIX级别编程环境》在样本:
#include <pthread.h>
struct msg {
struct msg *m_next;
/* ... more stuff here ... */
};
struct msg *workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
void process_msg(void) {
struct msg *mp;
for (;;) {
pthread_mutex_lock(&qlock);
while (workq == NULL)
pthread_cond_wait(&qready, &qlock);
mp = workq;
workq = mp->m_next;
pthread_mutex_unlock(&qlock);
/* now process the message mp */
}
}
void enqueue_msg(struct msg *mp) {
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}
一个简单的消息生产者和消费者的代码。
它们之间用condition同步。
这个代码最easy让人搞混的是process_msg函数里的pthread_mutex_lock 和 pthread_mutex_unlock 是一对函数调用,前面加锁,后面解锁。的确。是加锁解锁,可是它们两不是一对的。它们的还有一半在pthread_cond_wait函数里。
pthread_cond_wait函数能够觉得它做了三件事:
- 把自身线程放到condition的等待队列里,把mutex解锁;
- 等待被唤醒(当其他线程调用pthread_cond_signal或者pthread_cond_broadcast时)。
- 被唤醒之后。对metex加锁,再返回。
mutex和condition实际上是绑定在一起的,一个condition仅仅能相应一个mutex。在Java的代码里。Condition对象仅仅能通过lock.newCondition()的函数来获取。
Spurious wakeup
所谓的spurious wakeup。指的是一个线程调用pthread_cond_signal()。却有可能不止一个线程被唤醒。为什么会出现这样的情况?wiki和其他的一些文档都仅仅是说在多核的情况下。简化实现同意出现这样的spurious wakeup。
。
在man文档里给出了一个可能的实现,然后解析为什么会出现。
假定有三个线程,线程A正在运行pthread_cond_wait,线程B正在运行pthread_cond_signal,线程C正准备运行pthread_cond_wait函数。
pthread_cond_wait(mutex, cond):
value = cond->value; /* 1 */
pthread_mutex_unlock(mutex); /* 2 */
pthread_mutex_lock(cond->mutex); /* 10 */
if (value == cond->value) { /* 11 */
me->next_cond = cond->waiter;
cond->waiter = me;
pthread_mutex_unlock(cond->mutex);
unable_to_run(me);
} else
pthread_mutex_unlock(cond->mutex); /* 12 */
pthread_mutex_lock(mutex); /* 13 */ pthread_cond_signal(cond):
pthread_mutex_lock(cond->mutex); /* 3 */
cond->value++; /* 4 */
if (cond->waiter) { /* 5 */
sleeper = cond->waiter; /* 6 */
cond->waiter = sleeper->next_cond; /* 7 */
able_to_run(sleeper); /* 8 */
}
pthread_mutex_unlock(cond->mutex); /* 9 */
线程A运行了第1,2步,这时它释放了mutex。然后线程B拿到了这个mutext,而且pthread_cond_signal函数时运行并返回了。于是线程B就是一个所谓的“spurious wakeup”。
为什么pthread_cond_wait函数里一进入。就释放了mutex?没有找到什么解析。。
查看了glibc的源码,大概能够看出上面的一些影子,可是太复杂了,也没有搞明确为什么。。
/build/buildd/eglibc-2.19/nptl/pthread_cond_wait.c
/build/buildd/eglibc-2.19/nptl/pthread_cond_signal.c
只是从上面的解析,能够发现《UNIX高级编程》里的说明是错误的(可能是由于太久了)。
The caller passes it locked to the function, which then atomically places the calling thread on the list of threads waiting for the condition and unlocks the mutex.
上面的伪代码,一进入pthread_cond_wait函数就释放了mutex,明显和书里的不一样。
wait morphing优化
在《UNIX环境高级编程》的演示样例代码里,是先调用pthread_mutex_unlock,再调用pthread_cond_signal。
void enqueue_msg(struct msg *mp) {
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_mutex_unlock(&qlock);
pthread_cond_signal(&qready);
}
有的地方给出的是先调用pthread_cond_signal,再调用pthread_mutex_unlock:
void enqueue_msg(struct msg *mp) {
pthread_mutex_lock(&qlock);
mp->m_next = workq;
workq = mp;
pthread_cond_signal(&qready);
pthread_mutex_unlock(&qlock);
}
先unlock再signal,这有个优点。就是调用enqueue_msg的线程能够再次參与mutex的竞争中,这样意味着能够连续放入多个消息。这个可能会提高效率。类似Java里ReentrantLock的非公平模式。
网上有些文章说,先singal再unlock,有可能会出现一种情况是被singal唤醒的线程会由于不能立即拿到mutex(还没被释放)。从而会再次休眠。这样影响了效率。从而会有一个叫“wait morphing”优化,就是假设线程被唤醒可是不能获取到mutex,则线程被转移(morphing)到mutex的等待队列里。
可是我查看了下glibc的源码,貌似没有发现有这样的“wait morphing”优化。
man文档里提到:
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().
可见在调用singal之前,能够不持有mutex,除非是“predictable scheduling”,可预測的调度行为。这样的可能是实时系统才有这样的严格的要求。
为什么要用while循环来推断条件是否成立?
while (workq == NULL)
pthread_cond_wait(&qready, &qlock);
而不用if来推断?
if (workq == NULL)
pthread_cond_wait(&qready, &qlock);
一个原因是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
在这篇文章里,http://www.cppblog.com/Solstice/archive/2013/09/09/203094.html
给出的演示样例代码7里,觉得调用pthread_cond_broadcast来唤醒全部的线程是比較好的写法。可是我觉得pthread_cond_signal和pthread_cond_broadcast是两个不同东东。不能简单合并在同一个函数调用。仅仅唤醒一个效率和唤醒全部等待线程的效率显然不能等同。典型的condition是用CLH或者MCS来实现的,要通知全部的线程,则要历遍链表,显然效率减少。另外,C++11里的condition_variable也提供了notify_one函数。
http://en.cppreference.com/w/cpp/thread/condition_variable/notify_one
mutex,condition是不是公平(fair)的?
这个在參考文档里没有说明,在网上找了些资料,也没有什么明白的答案。
我写了个代码測试。发现mutex是公平的。
condition的測试结果也是差点儿相同。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; volatile int mutexCount = 0;
void mutexFairTest(){
int localCount = 0;
while(1){
pthread_mutex_lock(&lock);
__sync_fetch_and_add(&mutexCount, 1);
localCount += 1;
if(mutexCount > 100000000){
break;
}
pthread_mutex_unlock(&lock);
}
pthread_mutex_unlock(&lock);
printf("localCount:%d\n", localCount);
} int main() {
pthread_mutex_lock(&lock);
pthread_create(new pthread_t, NULL, (void * (*)(void *))&mutexFairTest, NULL);
pthread_create(new pthread_t, NULL, (void * (*)(void *))&mutexFairTest, NULL);
pthread_create(new pthread_t, NULL, (void * (*)(void *))&mutexFairTest, NULL);
pthread_create(new pthread_t, NULL, (void * (*)(void *))&mutexFairTest, NULL);
pthread_create(new pthread_t, NULL, (void * (*)(void *))&mutexFairTest, NULL);
pthread_create(new pthread_t, NULL, (void * (*)(void *))&mutexFairTest, NULL);
pthread_mutex_unlock(&lock); sleep(100);
}
输出结果是:
localCount:16930422
localCount:16525616
localCount:16850294
localCount:16129844
localCount:17329693
localCount:16234137
还特意在一个单CPU的虚拟机上測试了下。
输出的结果差点儿相同。操作系统是ububtu14.04。
连续调用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一直等待的情况。
可是,我们再细致考虑下:
怎样确认线程a, b 调用pthread_cond_wait完毕了?还是仅仅是刚切换到内核态?显然是没有办法知道的。
所以,我们平时编程肯定不会写这种代码,应该是共享变量。在获取到锁之后,再改动变量。
这样子来做同步。參考上面《UNIX环境高级编程》的样例。
只是,这个问题也是挺有意思的。
參考:
http://en.wikipedia.org/wiki/Spurious_wakeup
http://siwind.iteye.com/blog/1469216
http://www.cppblog.com/Solstice/archive/2013/09/09/203094.html
http://www.cs.cmu.edu/afs/cs/academic/class/15492-f07/www/pthreads.html
版权声明:本文博主原创文章,博客,未经同意不得转载。
并行编程条件变量(posix condition variables)的更多相关文章
- c++11多线程记录6:条件变量(condition variables)
https://www.youtube.com/watch?v=13dFggo4t_I视频地址 实例1 考虑这样一个场景:存在一个全局队列deque,线程A向deque中推入数据(写),线程B从deq ...
- C++11并行编程-条件变量(condition_variable)详细说明
<condition_variable >头文件主要包含有类和函数相关的条件变量. 包括相关类 std::condition_variable和 std::condition_variab ...
- 第8章 用户模式下的线程同步(4)_条件变量(Condition Variable)
8.6 条件变量(Condition Variables)——可利用临界区或SRWLock锁来实现 8.6.1 条件变量的使用 (1)条件变量机制就是为了简化 “生产者-消费者”问题而设计的一种线程同 ...
- c++并发编程之条件变量(Condition Variable)
条件变量(Condition Variable)的一般用法是:线程 A 等待某个条件并挂起,直到线程 B 设置了这个条件,并通知条件变量,然后线程 A 被唤醒.经典的「生产者-消费者」问题就可以用条件 ...
- POSIX多线程编程-条件变量pthread_cond_t
条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用.使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化.一旦其它 ...
- C++并发编程 条件变量 condition_variable,线程安全队列示例
1. 背景 c++11中提供了对线程与条件变量的更好支持,对于写多线程程序方便了很多. 再看c++并发编程,记一下学习笔记. 2. c++11 提供的相关api 3.1 wait wait用于无条件等 ...
- Windows:C++11并发编程-条件变量(condition_variable)详解
<condition_variable >头文件主要包含了与条件变量相关的类和函数.相关的类包括 std::condition_variable和 std::condition_varia ...
- Linux系统编程—条件变量
条件变量是用来等待线程而不是上锁的,条件变量通常和互斥锁一起使用.条件变量之所以要和互斥锁一起使用,主要是因为互斥锁的一个明显的特点就是它只有两种状态:锁定和非锁定,而条件变量可以通过允许线程阻塞和等 ...
- 4.锁--并行编程之条件变量(posix condition variables)
在整理Java LockSupport.park()的东东.看到了个"Spurious wakeup".又一次梳理下. 首先来个<UNIX环境高级编程>里的样例: [c ...
随机推荐
- Android----------WindowManager
我们Android平台是一个又一个的Activity组成的,每个Activity有一个或者多个View构成.所以说.当我们想显示一个界面的时候,我们首先想到的是建立一个Activity,然后全部的操作 ...
- Hadoop-2.2.0中国文献—— MapReduce 下一代 -- 公平调度
目的 此文档描写叙述了 FairScheduler, Hadoop 的一个可插入式的调度器.同意 YARN 应用在一个大集群中公平地共享资源. 简单介绍 公平调度是一种分配资源给应用的方法,以致到最后 ...
- CentOS 6.5 配置 SSDB 1.8.0
环境说明: OS:CentOS 6.5 (阿里云ECS) 相关链接: 1.SSDB 下载配置:http://ssdb.io/docs/install.html 2.SSDB 入门文档:http:// ...
- Eclipse扩展点实践之添加菜单项(ActionSet方式实现)
ActionSet方式比起Command方式,比较直观,但是功能有限. 首先:新建一个项目,在Extension中添加org.eclipse.ui.actionSets的扩展. 然后,new-> ...
- Storm-0.9.2-incubating源代码编译打包
近期遇到一些同学询问Storm-0.9.2-incubating源代码编译打包的问题,现将编译步骤说明例如以下: 1.凝视掉project各pom文件里关于maven插件(maven-gpg-plug ...
- start_kernel——boot_init_stack_canary
/* * Initialize the stackprotector canary value. * * NOTE: this must only be called from functions t ...
- 《JavaScript设计模式与开发实践》读书笔记之代理模式
1.代理模式 代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问 1.1 一般的图片加载 var myImage=(function () { var imgNode=document.c ...
- 安装ecshop提示“安装数据失败”或者“创建管理员帐号”
解决方法: 在install/includes/init.php文件的顶部,<?php 下增加: date_default_timezone_set ('Asia/Shanghai'); 即可 ...
- jdbc初步(转)
1. Jdbc的六个编程步骤 1. 注册一个驱动 注册驱动程序有三种方式: 方式一:Class.forName(“oracle.jdbc.driver.OracleDriver”); JAVA 规范中 ...
- Linux应用环境实战10:Bash脚本编程语言中的美学与哲学(转)
阅读目录 一.一切皆是字符串 二.引用和元字符 三.字符串从哪里来.到哪里去 四.再加上一点点的定义,就可以推导出整个Bash脚本语言的语法了 五.输入输出重定向 六.Bash脚本语言的美学:大道至简 ...