转载~kxcfzyk:Linux C语言多线程库Pthread中条件变量的的正确用法逐步详解
Linux C语言多线程库Pthread中条件变量的的正确用法逐步详解
(本文的读者定位是了解Pthread常用多线程API和Pthread互斥锁,但是对条件变量完全不知道或者不完全了解的人群。如果您对这些都没什么概念,可能需要先了解一些基础知识)
关于条件变量典型的实际应用,可以参考非常精简的Linux线程池实现(一)——使用互斥锁和条件变量,但如果对条件变量不熟悉最好先看完本文。
Pthread库的条件变量机制的主要API有三个:
- int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
- int pthread_cond_broadcast(pthread_cond_t *cond);
- int pthread_cond_signal(pthread_cond_t *cond);
注:还有一个没说的API是pthread_cond_timewait,它跟pthread_cond_wait(作用见下面)的唯一不同就是可以指定一个等待的超时时间,这里不对它作额外讨论。
它们和其它几个Pthread API一起用于处理一种特定情形的线程同步问题:
- 若干个线程在某个条件没满足时不能继续往下面走,于是纷纷调用pthread_cond_wait使自己在这个条件上陷入等待(休眠);
- 当条件满足以后,另外有个活跃着的线程调用pthread_cond_broadcast通知(唤醒)刚才那些等待在这个条件上的所有线程,让它们继续往下运行。
这种情形是非常通用、非常基础的,很多更加具体的线程同步问题都是这种情形的扩展,比如说经典的消费者/生产者问题,读者/写者问题等等。明显“条件”是这种情形的核心,所以Pthread的这套线程同步机制叫做“条件变量”。可以看出条件变量机制跟Java的wait/notify机制非常类似。
上面这种情形也可以用POSIX定义的另外一套线程/进程同步机制来实现——信号量(semaphore),而且信号量机制在实际场景中用起来比条件变量机制还简单一些,但是信号量的性能不如Pthread库中的条件变量。
条件变量通过pthread_cond_t数据类型来声明,而且使用之前必须先要初始化:
- pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
上面这种是通过预定义的初始化宏来静态初始化,也可以用函数动态初始化:
- pthread_cond_t cond;
- pthread_cond_init(&cond, NULL);
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);
注意条件变量应该声明为全局可见的,因为条件变量会在多个线程(的函数)中被访问。条件变量最后不再使用了的时候应该销毁:
- pthread_cond_destroy(&cond);
pthread_cond_destroy(&cond);
在初始化后到销毁前这段时间内就是条件变量的正常生命周期了,可以按需要对它调用pthread_cond_wait、pthread_cond_signal和pthread_cond_broadcast。
pthread_cond_signal的作用跟pthread_cond_broadcast相似,但不同的是pthread_cond_signal会通知所有等待线程中的至少一个,让它(们)继续往下运行,而所有其它没被通知的等待线程则继续等待(休眠)。之所以pthread_cond_signal并不是严格地只唤醒一个等待线程,是因为在多处理器或多核系统中,可能无法实现只唤醒一个等待线程,就算能强行做到只唤醒一个等待线程,也会带来很大的性能损失,这对一个通用的基础线程同步API来说并不合适。
但实际应用场景中我们通常希望每调用一次pthread_cond_signal就唤醒一个等待线程,比如说下面这种情况:
某个线程专门负责从网络接收数据包,其它若干线程专门负责处理数据包。当没有任何数据包时,处理线程全部调用pthread_cond_wait陷入等待。当一个数据包到达时,接收线程调用phtread_cond_signal唤醒一个处理线程,处理线程拿走这个数据包去处理。当又一个数据包到达时,接收线程再次调用pthread_cond_signal唤醒一个线程……
这个问题对信号量机制来说很容易,因为信号量中的sem_post函数只会唤醒一个等待的进程或线程。虽然pthread_cond_signal本身不保证只唤醒一个等待线程,但是POSIX标准在定义这套API时考虑过了这个问题,它留了一个“后门”,让我们在应用程序中可以通过额外的代码来解决这个问题。
先不考虑那个所谓的“后门”,一个粗略看上去可行的解决办法是,除了条件变量以外,再额外设置一个全局的普通计数变量表示允许唤醒多少个等待线程:
- int global_count=0;
int global_count=0;
那么当通知线程需要调用pthread_cond_signal唤醒别的等待线程之前,应该先增加全局变量的计数,表示允许唤醒的线程数目又增加了一个:
- global_count++;
- pthread_cond_signal(&cond);
global_count++;
pthread_cond_signal(&cond);
pthread_cond_signal一调用,那些调用pthread_cond_wait等待在cond的线程可能会有好几个都唤醒了,索性假设全部都被唤醒了。但其实我们只想让其中一个继续往下走,其它的不应该往下走,那么其它那些等待线程就都应该再次调用pthread_cond_wait继续等待(这里明显该有个循环)。
下面的问题就是,怎么决定哪一个等待线程继续走呢?可以这样,当大家都被唤醒的时候,大家都判断一下global_count是不是大于0,也就是当前允不允许唤醒线程。如果某个等待线程检测到的global_count是大于0的,就赶紧把global_count减掉一个,然后自己往下走。这时候global_count少了一个,可能就是0了,表示不允许再唤醒线程,其它几个等待线程发现这一状况以后就不往下走,再次调用pthread_cond_wait继续等待:
- while(global_count<=0) {
- pthread_cond_wait(&cond, ...);
- }
- global_count--;
while(global_count<=0) {
pthread_cond_wait(&cond, ...);
}
global_count--;
到现在为止问题基本解决了,但是引出了一个新的问题:多个线程同时访问global_count变量会造成竞态条件。问题看上去很容易解决,使用互斥锁保护就好了。
对于通知线程线程:
- pthread_mutex_lock(&mutex);
- global_count++;
- pthread_cond_signal(&cond);
- pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
global_count++;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
对于等待线程:
- pthread_mutex_lock(&mutex);
- while(global_count<=0) {
- pthread_cond_wait(&cond, ...);
- }
- global_count--;
- pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
while(global_count<=0) {
pthread_cond_wait(&cond, ...);
}
global_count--;
pthread_mutex_unlock(&mutex);
新的问题又出来了:等待线程调用pthread_cond_wait陷入等待时,还占有着mutex互斥锁,下次通知线程想要唤醒线程时就无法获取mutex互斥锁了,于是出现了死锁。所以在调用pthread_cond_wait将当前线程陷入等待之前,我们应该解开mutex互斥锁,当线程被唤醒,从pthread_cond_wait函数返回时,我们应该重新获取mutex互斥锁。
比如像这样:
- pthread_mutex_lock(&mutex);
- while(global_count<=0) {
- pthread_mutex_unlock(&mutex);
- pthread_cond_wait(&cond, ...);
- pthread_mutex_lock(&mutex);
- }
- global_count--;
- pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
while(global_count<=0) {
pthread_mutex_unlock(&mutex);
pthread_cond_wait(&cond, ...);
pthread_mutex_lock(&mutex);
}
global_count--;
pthread_mutex_unlock(&mutex);
这段代码还是有问题的,在pthread_cond_wait函数调用的前后当前线程都有一段看上去“很短”的不拥有mutex互斥锁的真空期,但是对于CPU来说这段真空期并不算太短。
假设某个等待线程检测到global_count==0,于是解开mutex互斥锁,进入真空期,即将调用pthread_cond_wait。就在这时候,通知线程增加了一下global_count的计数值然后调用了pthread_cond_signal。接下来,刚才那个等待线程调用pthread_cond_wait陷入等待,由于pthread_cond_wait的调用发生在pthread_cond_signal之后,所以pthread_cond_wait并不会返回。如果程序里的等待线程就这一个,这个通知就丢失了。 问题到了这里似乎没路可走了,但是别忘了还有个“后门”没用上,那就是前面一直没提的pthread_cond_wait的第二个参数了。看看本文最开始列出的函数声明,第二个参数赫然是mutex!这下猜也能猜到这第二个参数是干嘛的了,明显就是专门帮我们解开mutex锁啊,然后在pthread_cond_wait返回之前再自动获取mutex锁。这里顺道澄清一下,和条件变量关联的mutex,不是像网上部分人说的那样是用来保护条件变量的,条件变量在实现的时候是能够做到线程安全的,因为它内部还有一个自己的互斥锁。
所以正确的做法是:
- pthread_mutex_lock(&mutex);
- while(global_count<=0) {
- pthread_cond_wait(&cond, &mutex);
- }
- global_count--;
- pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
while(global_count<=0) {
pthread_cond_wait(&cond, &mutex);
}
global_count--;
pthread_mutex_unlock(&mutex);
到这里问题是不是完全解决了?很遗憾还差一点。pthread_cond_wait是线程撤销点(cancellation points)之一,这意味着当某个线程因为调用pthread_cond_wait而陷入休眠等待时,别的线程可以通过这个线程的ID调用pthread_cancel让这个线程强制从pthread_cond_wait返回并开始执行一些清理工作,最后结束退出。
问题就出在pthread_cond_wait返回上,上面标红的地方已经强调了,pthread_cond_wait返回之前会先自动获取mutex,也就是说返回以后已经占有了mutex互斥锁。这种情况下线程直接退出会导致互斥锁一直被占用,其它线程就无法获取这个互斥锁了,再次出现死锁。 这个问题有两种解决办法,第一个办法是在线程退出前的清理工作中加入解开互斥锁的代码,这个并不难办到,因为POSIX定义了两个API:
- void pthread_cleanup_pop(int execute);
- void pthread_cleanup_push(void (*routine)(void*), void *arg);
void pthread_cleanup_pop(int execute);
void pthread_cleanup_push(void (*routine)(void*), void *arg);
pthread_cleanup_push用于向一个特殊栈压入一个函数指针,当线程退出时,这个特殊栈中的所有函数会被一个个从栈顶弹出并执行(return退出的情况除外)。phtread_cleanup_pop用于从这个特殊栈的栈顶手动弹出函数指针,execute参数非0时,弹出的函数会被自动执行。
需要注意的是,POSIX标准允许这两个API被实现为带未闭合花括号的宏,所以这两个API一定(最好)要配套使用:它们必须一前一后(push在前),而且在同一个函数的同一个嵌套层次内。
比如说这两个API的实现有可能会是类似于这样:
- #define pthread_cleanup_pop(execute) XXXX { XXXX
- #define pthread_cleanup_push(routine, arg) XXXX } XXXX
#define pthread_cleanup_pop(execute) XXXX { XXXX
#define pthread_cleanup_push(routine, arg) XXXX } XXXX
所以这就是为什么它们的调用要求如此奇怪了。 有了这两个API,想要解决刚才的问题,首先要定义一个清理回调函数:
- void mutex_clean(void *mutex) {
- pthread_mutex_unlock((pthread_mutex_t*)mutex);
- }
void mutex_clean(void *mutex) {
pthread_mutex_unlock((pthread_mutex_t*)mutex);
}
然后在等待线程里调用那两个API:
- pthread_mutex_lock(&mutex);
- pthread_cleanup_push(mutex_clean, &mutex);
- while(global_count<=0) {
- pthread_cond_wait(&cond, &mutex);
- }
- global_count--;
- pthread_cleanup_pop(0);
- pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
pthread_cleanup_push(mutex_clean, &mutex);
while(global_count<=0) {
pthread_cond_wait(&cond, &mutex);
}
global_count--;
pthread_cleanup_pop(0);
pthread_mutex_unlock(&mutex);
或者一个稍微简洁一些的写法:
- pthread_mutex_lock(&mutex);
- pthread_cleanup_push(mutex_clean, &mutex);
- while(global_count<=0) {
- pthread_cond_wait(&cond, &mutex);
- }
- global_count--;
- pthread_cleanup_pop(1);
pthread_mutex_lock(&mutex);
pthread_cleanup_push(mutex_clean, &mutex);
while(global_count<=0) {
pthread_cond_wait(&cond, &mutex);
}
global_count--;
pthread_cleanup_pop(1);
另外一种解决方案比这个麻烦一些,那就是设置mutex互斥锁的robust属性值为PTHREAD_MUTEX_ROBUST。
对于robust互斥锁,当持有它的线程没解锁就退出以后,别的线程再去调用pthread_mutex_lock,函数会回一个EOWNERDEAD错误,线程检测到这这个错误后可以调用pthread_mutex_consistent使robust互斥锁恢复一致性,紧接着就可以调用phtread_mutex_unlock解锁了(尽管这个锁并不是当前这个线程加持的)。解锁完毕就可以重新调用pthread_mutex_lock了。 采用这种用方案的时候,首先要声明一个全局可见的mutex属性变量:
- pthread_mutexattr_t mutexattr;
pthread_mutexattr_t mutexattr;
然后初始化并设置属性值:
- pthread_mutexattr_init(&mutexattr);
- pthread_mutexattr_setrobust(&mutexaddtr, PTHREAD_MUTEX_ROBUST);
pthread_mutexattr_init(&mutexattr);
pthread_mutexattr_setrobust(&mutexaddtr, PTHREAD_MUTEX_ROBUST);
有了mutex属性,接下来就是在初始化mutex的地方作修改了,通常我们对mutex的初始化都是pthread_mutex_init(&mutex, NULL),现在改成:
- pthread_mutex_init(&mutex, &mutexattr);
pthread_mutex_init(&mutex, &mutexattr);
现在准备工作已经完毕,开始干正事了。对于通知线程:
- while(EOWNERDEAD==pthread_mutex_lock(&mutex)) {
- pthread_mutex_consistent(&mutex);
- pthread_mutex_unlock(&mutex);
- }
- global_count++;
- pthread_cond_signal(&cond);
- pthread_mutex_unlock(&mutex);
while(EOWNERDEAD==pthread_mutex_lock(&mutex)) {
pthread_mutex_consistent(&mutex);
pthread_mutex_unlock(&mutex);
}
global_count++;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
对于等待线程:
- while(EOWNERDEAD==pthread_mutex_lock(&mutex)) {
- pthread_mutex_consistent(&mutex);
- pthread_mutex_unlock(&mutex);
- }
- while(global_count<=0) {
- pthread_cond_wait(&cond, &mutex);
- }
- global_count--;
- pthread_mutex_unlock(&mutex);
while(EOWNERDEAD==pthread_mutex_lock(&mutex)) {
pthread_mutex_consistent(&mutex);
pthread_mutex_unlock(&mutex);
}
while(global_count<=0) {
pthread_cond_wait(&cond, &mutex);
}
global_count--;
pthread_mutex_unlock(&mutex);
到这里,所有的事情终于完成了!
转载~kxcfzyk:Linux C语言多线程库Pthread中条件变量的的正确用法逐步详解的更多相关文章
- Linux组件封装(二)中条件变量Condition的封装
条件变量主要用于实现线程之间的协作关系. pthread_cond_t常用的操作有: int pthread_cond_init(pthread_cond_t *cond, pthread_conda ...
- Linux多线程编程详细解析----条件变量 pthread_cond_t
Linux操作系统下的多线程编程详细解析----条件变量 1.初始化条件变量pthread_cond_init #include <pthread.h> int pthread_cond_ ...
- “全栈2019”Java多线程第二十八章:公平锁与非公平锁详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- “全栈2019”Java多线程第二十二章:饥饿线程(Starvation)详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java多 ...
- Linux中让alias设置永久生效的方法详解
Linux中让alias设置永久生效的方法详解 一.问题描述 1.有很多时候我们想要将很多操作作为一个步骤,那么在不作为系统的服务的情况下,别名是我们最好的选择,但是发现别名只能在一次会话中生效,重启 ...
- linux中cat、more、less命令区别详解##less 最合适最好用,和vim一样好用
linux中cat.more.less命令区别详解 caoxinyiyi关注 0.0362018.07.02 15:46:17字数 641阅读 516 linux中命令cat.more.less均可用 ...
- Linux C语言多线程编程实例解析
Linux系统下的多线程遵循POSIX线程接口,称为 pthread.编写Linux下的多线程程序,需要使用头文件pthread.h,连接时需要使用库libpthread.a.顺便说一下,Linux ...
- Linux操作系统下的多线程编程详细解析----条件变量
条件变量通过允许线程阻塞和等待另一个线程发送信号的方法,弥补了互斥锁(Mutex)的不足. 1.初始化条件变量pthread_cond_init #include <pthread.h> ...
- 非常精简的Linux线程池实现(一)——使用互斥锁和条件变量
线程池的含义跟它的名字一样,就是一个由许多线程组成的池子. 有了线程池,在程序中使用多线程变得简单.我们不用再自己去操心线程的创建.撤销.管理问题,有什么要消耗大量CPU时间的任务通通直接扔到线程池里 ...
随机推荐
- 去除手机端a标签等按下去背景色
a,button,input,textarea,label,i,em{/*highlight*/ -webkit-tap-highlight-color: rgba(255,0,0,0); borde ...
- bpl 包的编写和引用
转载:http://www.cnblogs.com/gxch/archive/2011/04/23/bpl.html 为什么要使用包? 答案很简单:因为包的功能强大.设计期包(design-time ...
- git 学习笔记2--How to create/clone a repository
1. create/clone 1.1 create 针对已经存在的目录创建一个repository,使用以下命令: git init Initialized empty Git repository ...
- ASP.NET MVC中使用highcharts 生成简单的折线图
直接上步骤: 生成一个options,选项包含了一些基本的配置,如标题,坐标刻度,serial等: 配置X轴显示的Category数据,为一个数组: 配置Y轴显示的数据,也为一个数据: 用 ...
- xampp的Apache无法启动解决方法
XAMPP Apache 无法启动原因1(缺少VC运行库): 这个就是我遇到的问题原因,下载安装的XAMPP版本是xampp-win32-1.7.7-VC9,而现有的Windows XP系统又没有安装 ...
- iOS学习06C语言结构体
1.结构体的概述 在C语言中,结构体(struct)指的是一种数据结构,是C语言中构造类型的其中之一. 在实际应用中,我们通常需要由不同类型的数据来构成一个整体,比如学生这个整体可以由姓名.年龄.身高 ...
- 【HDU3652】B-number 数位DP
B-number Problem Description A wqb-number, or B-number for short, is a non-negative integer whose de ...
- CSS3弹性盒模型flexbox布局基础版
原文链接:http://caibaojian.com/using-flexbox.html 最近看了社区上的一些关于flexbox的很多文章,感觉都没有我这篇文章实在,最重要的兼容性问题好多人都没有提 ...
- 自适应学习率调整:AdaDelta
Reference:ADADELTA: An Adaptive Learning Rate Method 超参数 超参数(Hyper-Parameter)是困扰神经网络训练的问题之一,因为这些参数不可 ...
- ACM 变态最大值
变态最大值 时间限制:1000 ms | 内存限制:65535 KB 难度:1 描述 Yougth讲课的时候考察了一下求三个数最大值这个问题,没想到大家掌握的这么烂,幸好在他的帮助下大家算是解 ...