来自知乎的pthread_cond_wait为什么总是带着mutex
在原来的答案中,有这样的代码:
pthread_mutex_unlock(mtx);
pthread_cond_just_wait(cv);
pthread_mutex_lock(mtx);
事实上,上面三行代码的并不是pthread_cond_wait(cv, mtx)的内联展开。其中第一行和第二行必须“原子化”,而第三行是可以分离出去的(之所以要把第三行放在里面的原因可以参见原来的答案)。
那么为什么第一行和第二行不能分离呢?这是因为必须得保证:如果线程A先进入wait函数(即使没有进入实际的等待状态,比如正在释放mtx),那么必须得保证其他线程在其之后调用的broadcast必须能够将线程A唤醒。
所以,把原来答案中的代码再贴一遍:
// 线程A,条件测试
pthread_mutex_lock(mtx); // a1
while(pass == 0) { // a2
pthread_mutex_unlock(mtx); // a3
pthread_cond_just_wait(cv); // a4
pthread_mutex_lock(mtx); // a5
}
pthread_mutex_unlock(mtx);
// 线程B,条件发生修改,对应的signal代码
pthread_mutex_lock(mtx); // b1
pass = 1; // b2
pthread_mutex_unlock(mtx); // b3
pthread_cond_signal(cv); // b4
如果执行序列是:a1, a2, a3, b1, b2, b3, b4, a4,那么线程A将不会被唤醒。而a3在线程B之前执行,这意味着wait函数是在signal之前调用的,所以不满足上文提到的保证。
解决办法:
- 先将线程附加到等待队列
- 释放mutex
- 进入等待
感兴趣的同学的可以看下源码(pthread_cond_wait.c),附加到等待队列这个操作是加锁的,所以可以保证之前发起的signal不会错误得唤醒本线程,而之后发起的signal必然唤醒本线程。
因此,下面的代码是绝对不会出错的:
// 线程A,条件测试
pthread_mutex_lock(mtx); // a1
while(pass == 0) { // a2
pthread_cond_wait(cv, mtx); // a3
}
pthread_mutex_unlock(mtx); // a4
// 线程B,条件发生修改,对应的signal代码
pthread_mutex_lock(mtx); // b1
pass = 1; // b2
pthread_mutex_unlock(mtx); // b3
pthread_cond_signal(cv); // b4
如果线程A先运行,那么执行序列必然是:a1, a2, a3, b1, b2, b3, b4, a4。
如果线程B先运行,那么执行序列可能是:b1, b2, b3, b4, a1, a2, a4
也可能是:b1, b2, b3, a1, a2, a3, b4, a4
所以,如果是我设计pthread API,那么我会添加一个pthread_cond_unlock_and_wait函数,伪代码如下:
int pthread_cond_wait(cv, mtx) {
int ret = pthread_cond_unlock_and_wait(cv, mtx);
pthread_mutex_lock(mtx);
return ret;
}
// 线程A,条件测试
pthread_mutex_lock(mtx);
if (pass == 0)
pthread_cond_unlock_and_wait(cv, mtx);
else
pthread_mutex_unlock(mtx);
// 线程B,条件发生修改,对应的signal代码
pthread_mutex_lock(mtx); // b1
pass = 1; // b2
pthread_mutex_unlock(mtx); // b3
pthread_cond_signal(cv); // b4
这样的好处在于:如果我们可以保证没有虚假唤醒(即不需要while循环测试条件),那么我们可以将线程A的代码改成上述形式,这样无论怎样都只需要执行一次pthread_mutex_unlock()函数,而之前的版本至少需要执行两次。
—————— 原来的答案——————
感谢大家的回答!
看了之后,我获得了启发,突然觉得这或许是跟条件变量的通常用法有关。
首先需要明白两点:
- wait()操作通常伴随着条件检测,如:
while(pass == 0)
pthread_cond_wait(...);
- signal*()函数通常伴随着条件改变,如:
pass = 1;
pthread_cond_signal(...)
由于此两处都涉及到变量pass,所以为了防止Race Condition,必须得加锁。所以代码会变成下面这样:
// 条件测试
pthread_mutex_lock(mtx);
while(pass == 0)
pthread_cond_wait(...);
pthread_mutex_unlock(mtx);
// 条件发生修改,对应的signal代码
pthread_mutex_lock(mtx);
pass = 1;
pthread_mutex_unlock(mtx);
pthread_cond_signal(...);
然后,我们假设wait()操作不会自动释放、获取锁,那么代码会变成这样:
// 条件测试
pthread_mutex_lock(mtx);
while(pass == 0) {
pthread_mutex_unlock(mtx);
pthread_cond_just_wait(cv);
pthread_mutex_lock(mtx);
}
pthread_mutex_unlock(mtx);
// 条件发生修改,对应的signal代码
pthread_mutex_lock(mtx);
pass = 1;
pthread_mutex_unlock(mtx);
pthread_cond_signal(cv);
久而久之,程序员发现unlock, just_wait, lock这三个操作始终得在一起。于是就提供了一个pthread_cond_wait()函数来同时完成这三个函数。
另外一个证据是,signal()函数是不需要传递mutex参数的,所以关于mutex参数是用于同步wait()和signal()函数的说法更加站不住脚。
所以我的结论是:传递的mutex并不是为了防止wait()函数内部的Race Condition!而是因为调用wait()之前你总是获得了某个mutex(例如用于解决此处pass变量的Race Condition的mutex),并且这个mutex在你调用wait()之前必须得释放掉,调用wait()之后必须得重新获取。
所以,pthread_cond_wait()函数不是一个细粒度的函数,却是一个实用的函数。
来自知乎的pthread_cond_wait为什么总是带着mutex的更多相关文章
- [转]一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程
一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程 希望此文能给初学多线程编程的朋友带来帮助,也希望牛人多多指出错误. 另外感谢以下链接的作者给予,给我的学习带来了很大帮助 http ...
- c++ 条件变量
.条件变量创建 静态创建:pthread_cond_t cond=PTHREAD_COND_INITIALIZER; 动态创建:pthread_cond _t cond; pthread_cond_i ...
- unity导出工程导入到iOS原生工程中详细步骤
一直想抽空整理一下unity原生工程导入iOS原生工程中的详细步骤.做iOS+vuforia+unity开发这么长时间了.从最初的小小白到现在的小白.中间趟过了好多的坑.也有一些的小小收货.做一个喜欢 ...
- 【转】 Linux下的多线程编程
作者:gnuhpc 出处:http://www.cnblogs.com/gnuhpc/原文链接:http://www.cnblogs.com/gnuhpc/archive/2012/12/07/280 ...
- Linux下的多线程编程
1 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的 Unix也支持线程的概念,但是在一个进程(proces ...
- 【转】Linux下的多线程编程
1 引言 线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者.传统的 Unix也支持线程的概念,但是在一个进程(proces ...
- 并行编程条件变量(posix condition variables)
在整理Java LockSupport.park()东方的,我看到了"Spurious wakeup",通过重新梳理. 首先,可以在<UNIX级别编程环境>在样本: # ...
- 条件变量signal与unlock的顺序
编写同步队列时,有用到条件变量,对操作队列的线程进行同步.当队列为空时,允许get线程挂起,直到add线程向队列添加元素并通过唤醒条件变量,get线程继续向下运行.条件变量在多线程程序中用来实现“等待 ...
- JDFS:一款分布式文件管理实用程序第一篇(线程池、epoll、上传、下载)
一 前言 截止目前,笔者在博客园上面已经发表了3篇关于网络下载的文章,这三篇博客实现了基于socket的http多线程远程断点下载实用程序.笔者打算在此基础上开发出一款分布式文件管理实用程序,截止目前 ...
随机推荐
- LeetCode Move Zeroes (简单题)
题意: 给定一个整型数组nums,要求将其中所有的0移动到末尾,并维护所有非0整数的相对位置不变. 思路: 扫一遍,两个指针维护0与非0的交界,将非0的数向前赋值就行了. C++ class Solu ...
- 面试官:自己搭建过vue开发环境吗?
开篇 前段时间,看到群里一些小伙伴面试的时候被面试官问到这类题目.平时大家开发vue项目的时候,相信大部分人都是使用 vue-cli脚手架生成的项目架构,然后 npm run install 安装依赖 ...
- C# 文件操作 常用的类
File------实用类,提供许多静态方法,用于移动.删除.和复制文件. Directory------实用类,提供许多静态方法,用于移动.删除和复制目录. Path------ 实用类,用于处理路 ...
- Linux命令安装vnc服务端与vnc的客户端
第一歩:运行命令 yum install tigervnc-server -y 第二歩:安装telnet 第三歩:运行vncserver,创建桌面 vncserver -kill :1 删除桌面1的 ...
- 解决使用Application Loader上传ipa提示“上传appstore失败”
试了好多次使用Application Loader上传ipa,一直提示上传失败,用其他mac电脑却可以,那就是环境有问题,笔者试过重装xcode,都无法解决问题, 查看日志类似是jdk版本问题,换了所 ...
- JS MarcoTasks MicroTasks
JS MarcoTasks MicroTasks 在JS的event loop中,有两种任务队列microtasks和macrotasks microtasks process.nextTick Pr ...
- KVM修改虚机网卡模式:由NAT模式改为Bridge模式
1)关闭虚机# virsh shutdown vm1 2)编辑虚机配置文件# virsh edit vm1 <interface type='default'> 改为<int ...
- pycharm永久激活记录
由于上一年安装的pycharm激活时是用的激活码,有期限的,一直到今年5月4日过期,这两天顺便把版本也更新到最新,一直用的free版,到今天提醒我free快到期了,所以才狠下心来去找解决方案,目前已经 ...
- 常用的几个JQuery代码片段
1. 导航菜单背景切换效果 在项目的前端页面里,相对于其它的导航菜单,激活的导航菜单需要设置不同的背景.这种效果实现的方式有很多种,下面是使用JQuery实现的一种方式: //注意:代码需要修饰完善 ...
- JZOJ 3388. 【NOIP2013模拟】绿豆蛙的归宿
3388. [NOIP2013模拟]绿豆蛙的归宿 (Standard IO) Time Limits: 1000 ms Memory Limits: 131072 KB Detailed Limi ...