说明
- 多线程编程中,时常会遇到需求:线程需要等待某些条件满足才能继续运行,而这些条件会在别的线程中发生变化。
- 为实现这种需求,应用层简单方法可能是:使用锁+轮询;使用锁来保护“条件变量”的访问,使用轮询的方式来修改和读取“条件变量”的值,然而该方式会有一些不便之处,如下:
- 频繁的锁定和释放锁会有一定的性能浪费。
- 轮询间隔不好把握,间隔时间太短,消耗的CPU资源较多,间隔时间太长,不能很及时的响应请求。
条件变量
- 条件变量就是解决以上问题的一种线程同步机制,其做法:采用阻塞线程和消息唤醒的方式可以极大程度上减少资源的浪费以及增加实时性。
- 从名字即可大致理解条件变量的作用:通过该全局变量(使用线程都能访问的变量)保存一个条件,线程可以以阻塞的方式等待该条件的成立,条件成立后线程继续运行,或者发送消息通知其他线程该条件已成立。
- 条件变量的使用有两个行为:
- 线程可以通过等待"条件变量的条件成立"而挂起,线程睡眠(等待调度,不占用CPU资源)。
- 线程也可以发出信号,告知其它线程:该条件变量的条件已成立。
- 由于涉及到多线程操作,为了防止访问公共的条件时产生错乱,条件变量的使用总是和一个互斥锁结合在一起。
Linux平台
- Linux平台下条件变量类型为:pthread_cond_t。
- 头文件:#include <pthread.h>。
- 需要链接库: -lpthread。
接口说明
创建
1. 动态方式
int pthread_cond_init(pthread_cond_t * cond, const pthread_condattr_t * attr);
2. 静态方式
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- 采用动态方式,如果attr为NULL,系统会使用默认值初始化一个条件变量cond,相当于采用静态方式创建条件变量。
系统时间/Monotonic时间
- 系统时间(默认)。
- MONOTONIC时间。
- 采用系统时间,如果使用过程中用户设置了系统时间,可能导致运行错乱,为了避免这种情况,可采用Monotonic时间,设置方法如下:
pthread_condattr_t condAttr;
pthread_condattr_init(&condAttr);
pthread_condattr_setclock(&condAttr, CLOCK_MONOTONIC));
等待条件变量条件成立
1. 持续等待
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
2. 超时等待
int pthread_cond_timedwait(pthread_cond_t *cond,pthread_mutex_t *mutex, const struct timespec *restrict abstime);
参数说明:
* cond :条件变量
* mutex : 互斥锁
* abstime :结束等待的绝对时间(不是相对时间)
- 执行流程:这两个函数执行开始会自动解锁mutex,阻塞等待条件变量成立,收到条件成立消息后会自动锁定mutex,因此调用该函数时应确保互斥锁mutex已锁定,否则可能导致未知错误。
- 常见用法如下:
pthread_mutex_lock(mutex);
pthread_cond_wait(cond, mutex); //或者调用:pthread_cond_timedwait
//TODO: 条件成立后的操作
pthread_mutex_unlock(mutex);
唤醒等待条件变量的线程
- 唤醒一个
int pthread_cond_signal(pthread_cond_t *cond);
- 唤醒所有
int pthread_cond_broadcast(pthread_cond_t *cond);
- 注意:如果没有被条件变量阻塞的线程,pthread_cond_signal与pthread_cond_broadcast也会返回正确,其产生的信号将被忽略,不会做保存。
- 注意:既然判断和睡眠是由互斥量来保护从而成为一个原子操作,那么其他改变条件的线程就应该以一致的方式修改条件
也就是说其他线程在改变条件状态前也必须首先锁住互斥量 - 常见用法:
pthread_mutex_lock(&__mutex)
pthread_cond_signal(&__cond)
pthread_mutex_unlock(&__mutex)
注销
int pthread_cond_destroy(pthread_cond_t *cond)
- 注意:只有在没有线程等待该条件变量时,才能注销这个条件变量,否则返回EBUSY。
常见问题
为什么条件变量的使用需要互斥锁配合
- 互斥锁是用来解决线程之间的竞争问题的,条件变量操作并不是原子操作,也不自带锁,所以需要由外部互斥量来保护的。
信号丢失问题
- 编程中可能出现信号丢失问题,即发送了信号但是却没有唤醒线程,原因:出问题的时候,没有满足条件变量的使用条件(先wait,后发信号),如果没有线程在等待条件变量信号,信号会被忽略。
- 异常使用
- 无间隔多次发送信号
* 等待信号
pthread_mutex_lock(&m_pthreadMutex);
while(1)
{
pthread_cond_wait(&m_pthreadCond,&m_pthreadMutex);
printf("wait-----------\n");
}
pthread_mutex_unlock(&m_pthreadMutex);
* 发送信号
cnt = 4;
while (cnt--)
{
pthread_mutex_lock(&m_pthreadMutex);
pthread_cond_signal(&m_pthreadCond);
pthread_mutex_unlock(&m_pthreadMutex);
}
* 可能出现:唤醒等待端后,发送信号端持续获得锁和释放锁。
虚假唤醒
- 编程中可能出现,等待线程被唤醒,但是其它线程没有发送信号的情况,原因可能是:信号中断。
- 编程中可能出现,使用signal发送信号,可能最终有多个线程被唤醒,原因可能是:signal唤醒消息并没有指向性,并不是只发给某一个线程,多个线程收到信号,在pthread_cond_wait函数处理中,可能出现一些线程被唤醒但是还未获取锁或其它操作,但是另一线程该函数已经执行完了,所以锁释放后,没获取到锁的线程可能会立即获取锁,并执行下一步。
使用
* 等待端
pthread_mutex_lock(&m_mutex);
while(cond != true){ //循环判断条件是否满足
pthread_cond_wait(&m_cond,&m_mutex);
}
//TODO: xxxx
pthread_unlock(&m_mutex);
* 信号发送端
pthread_mutex_lock(&mutex);
cond = true;
pthread_cond_signal(&m_cond);
pthread_mutex_unlock(&mutex);