线程(四):条件变量
- 作者: 雪山肥鱼
- 时间:20210424 08:32
- 目的:整理OSTEP 的 条件变量相关章节
# 为何需要条件变量
# 定义
## 一定需要while 和 全局变量 done吗
## 一定需要锁吗
# 生产者和与消费者
## 代码分析
### A Bronken Solution - CV
### Better, But still broken , while , NOT if
### The Single Buffer Producer/Cosumer Solution
# 覆盖性条件
为何需要条件变量(Condition Variables)
锁并不是唯一的多线程通信的方案。在其他一些case中,比如父进程在执行后续操作之前,要检查子进程是否结束。也就是说父进程被block,子进程complete后,需要通知父进程,然后父进程才能继续运行下去。
volatile int done = 0;
void * child(void * arg) {
printf("child\n");
done = 1;
return NULL:
}
int main(int argc, char ** argv) {
printf("parent: begin\n");
pthread_t c;
pthread_create(&c, NULL, child, NULL); // create child
while (done == 0)
; // spin
printf("parent:end \n");
return 0;
}
在父进程中采用自旋的方式,其实非常低效,浪费CPU周期。而且有时候正确性也不能保证。此时在这种A线程需要等待B线程的通知才能进行下去的情况,我们可以使用条件变量,condition variable.
定义
条件变量是一个队列,线程可以将他们自己放入其中,睡眠,等待条件满足被唤醒(当然被唤醒可以不止一个)。
变量类型:pthread_cond_t c
操作动作(Posix call):**pthread_cond_wait(pthread_cond_t c, pthread_mutex_t m)
其实就是wait + signal 的操作
int done = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t c = PTHREAD_COND_INITIALIZER;
void * child(void * arg) {
printf("child\n");
thr_exit();
return NULL;
}
void thr_exit() {
pthread_mutex_lock(&m);
done = 1;
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
}
void thr_join() {
pthread_mutex_lock(&m);
while(done == 0)
pthread_cond_wait(&c, &m);
pthread_mutex_unlock(&m);
}
int main(int argc, char ** argv) {
printf("parent: begin\n");
pthread_t p ;
pthread_create(&p, NULL, child, NULL
thr_join();
printf("parent:end\n");
return 0;
}
wait 函数是携带一把锁的,在该线程wait之前,该线程是拿到这把锁的。调用wait时,该线程释放这把锁后,自行进入sleep队列。
如果该线程被唤醒,那么该线程一定已经再次 re-acuqire这把锁了。然后才从wait函数返回。
对于上面一段代码会有两种情况
-
情况1:
- 线程1在create 出线程2后,cpu调度,继续执行线程1
- 线程1进入thr_join()函数,拿到锁,检查done==0,线程1进入wait,睡眠
- 线程2拿到锁,将done置1,通知条件变量c,唤醒线程1,释放锁
- 线程1被唤醒,切拿到锁,此时done == 1, 则打印出 parent
-
情况2:
- 线程1在create出线程2后,cpu调度,先执行了线程2
- 线程2拿到锁,将done置1, 通知条件变量c,c下但此时没有线程睡眠,直接返回,结束
- 线程1中,dong != 0 直接打出parent:end 结束
-
疑问1:
一定需要全局变量done 和 while吗?以下的代码不行吗?
void thr_exit() {
pthread_mutex_lock(&m);
pthread_cond_signal(&c);
pthread_mutex_unlock(&m);
}
void thr_join() {
pthread_mutex_lock(&m);
pthread_cond_wait(&c, &m);
pthread_mutex_unlock(&m);
}
缺陷:子线程先被调用后,无睡眠signal,该条件变量没有下挂的睡眠现成,则子线程立刻返回,父线程拿到锁,进入condi睡眠,则无人再将其唤醒。增加while 和 done 其实就是为了过滤这种情况。此时,就不用再进入condition, 让父线程睡了。
- 疑问2:
一定要锁吗?
void thr_exit() {
done = 1;
pthread_cond_signal(&c);
}
void thr_joine() {
if(done == 0)
pthread_cond_wait(&c);
}
这里会有一个狡猾的race condition.
在thr_join函数中,当父线程检查了done的值后,在调用wait之前,被中断,执行了子线程。子线程中将done 置1。因为无线程sleep,直接返回
回到父线程,父线程进入睡眠,且done = 1,无人再次唤醒父进程。
条件变量中signal别人,还是自己wait 必带锁,来保护 变量 done,上述代码的c,只是下挂了一个 queue 起到通知的作用。我的理解是这样的,由程序员决定done是否满足条件,将queue里的线程唤醒。
Hold the lock when calling signal or wait
生产者与消费者问题
生产者可以多个线程,消费者也可以多个线程。我们经常用的linux grep命令也是生产者和消费者模型。
grep foo file.txt | wc -l
在linux中涉及两个进程 grep 与 wc。
- grep 将file.txt中含有foo字符串的行 输入到standard output,标准输出
- Linux 将 结果 redirect 重定向到 pipe 中
- 另一个进程wc 的 标准输出 standard output 对接到 pipe 中的另一端。
- grep 负责生产,wc 负责消费
代码分析
A Bronken Solution - CV
int buffer;
int count = 0; // initially, empty
cond_t cond;
mutex_t mutex;
void put(int value) {
assert(count == 0);
count = 1;
buffer = value;
}
int get() {
assert(count == 1);
count = 0;
return buffer;
}
void * producer(void *arg) {
int i;
for( i = 0; i < loops; i++) {
pthread_mutex_lock(&mutex);
if(count == 1)
pthread_cond_wait(&cond, &mutex);
put(i);
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
}
void * consumer(void * arg) {
int i ;
for(i = 0; i< loops; i++) {
pthread_mutex_lock(&mutex);
if(count == 0)
pthread_cond_wait(&cond, &mutex);
int temp = get();
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
printf("%d\n", tmp)
}
}
只有1个消费者和1个生产者以上代码没有什么问题。让我们看下多个消费者的线程和一个生产这的情况。
问题:
- 当Tc1 睡眠后,Tp 生产,signal condition,将Tc1 从sleeb'bp状态 转换到 ready 状态。但是此时注意Tc1还没有运行。
- 就在这时候 Tc2 抢占了CPU,插入进来,将队列里data 偷走了。
- 当Tc1 再次运行起来,去get 数据,发现数据内容没了,走进了 get函数中的 assert,报错。
Better, But still broken , while , NOT if
解决方式很简单,将if 换成 while
while(count == 1)
pthread_cond_wait(&cond, &mutex);
while(count == 0)
pthread_cond_wait(&cond, &mutex);
condition variable is to always use while loops! Sometimes you don't have to recheck the condition, buut it is always safe to do so
即使修改成while以后,依然存在bug. 假设2个Tc线程,1个Tp线程,会造成 all sleep的现象。或者Tp的生产速度很慢。注意,这里条件变量只有一个 cond

- Tc1 Tc2 先于 Tp1 运行。均没有资源,进入sleep状态。Tp被调度,生产1个资源。同时signal 给 condition, Tc1 得到调用,处于ready状态(由睡眠队列的shedule进行调度,决定Tc1还是Tc2,从睡眠队列出来 Tc2 Tp 处于睡眠队列
- Tc1 消费了 资源后,signal(&cond),此时激活那个线程呢?Tc2, Tp? 同样 此时由睡眠队列的调度决定。如果Tc2 处于ready,那么Tc2发现没有资源继续睡,那么很容易出现Tc1 Tc2 Tp三者同时sleep。同时没有 signal 函数促触发,全部睡死。
因为pthread_cond_signal唤醒的是相关条件变量cond,cond下挂的睡眠队列,谁先被唤醒,是基于这个队列的管理方式。同时,线程被唤醒,然后才是拿到锁!
which is definitely possible, depending on how the wait queue is managed.
问题的结症在于,Consumer 线程 不应该去 signal 其他 Consumer线程,只能signal Producer线程
The Single Buffer Producer/Cosumer Solution
cond_t empty, fill;
mutex_t mutex;
void * producer(void * arg) {
int i;
for(i = 0; i< loops; i++) {
pthread_mutex_lock(&mutex);
while(count == 1)
pthread_cond_wait(&empty, &mutex);
put(i);
pthread_cond_signal(&fill);
pthread_mutex_unlock(&mutex);
}
}
void * consumer(void * arg) {
int i ;
for(i = 0;i < loops; i++) {
pthread_mutex_lock(&mutex);
while(count == 0)
pthread_cond_wait(&fill, &mutex);
int tmp = get();
pthread_cond_signal(&empty);
pthread_mutex_unlock(&mutex);
printf("%d\n",tmp);
}
}
代码解读:
-
生产者优先被调用:
- 进入producer函数,上锁,count = 0
- 生产 唤醒 cond fill 下面挂的 消费者线程
2.1 生产资源,返回。释放锁- 生产过程中 消费者线程不可能打扰生产者进程,因为此时mutex在生产者手上
-
此时mutex 自由
- 被生产者抢到
上锁,睡眠在 cond empty下的queue中,交出锁 - 被消费者抢到
上锁,check count != 0 ,消费,激活empty下的睡眠队列的生产者线程,(没有也直接返回),release lock.
- 被生产者抢到
-
此时 mutex自由
- 被生产者抢到
则往复上面的流程 - 被消费者抢到
上锁,此时资源为0,睡在了cond fill下的queue中,同时交出锁,for也不执行了,因为一直睡,及时下次时间片来了,也不会去抢锁(暂时理解为上期队列锁的park())。
- 被生产者抢到
-
消费者优先被调用
- 进入消费者函数,上锁 cont =0
- 交出锁,将自己挂在cond fill的睡眠队列下, 线程队列不会在出来抢锁
- 父进程抢到锁,生产,激活fill 下的 消费者线程。丢掉锁
-
此时mutex自由
4.1 被producer 抢到,count == 1 ,生产者线程谁在cond empty 的睡眠队列下
4.2 被consumer抢到,recheck count != 0,消费,激活empty下的生产者线程,释放锁。
1. 由上述分析得知,signal负责激活cond下挂的睡眠队列中的线程,而wait 负责将线程睡眠在cond下的睡眠队列
2. 锁 针对临界区而言,可以理解为线程从睡眠队列激活后,才去争抢的
3. 对于cond fill 和 cond empty的理解
- 对于消费者来说,当cont == 0 也就是没有资源。挂在cond fill 下,等待被通知,直到有资源了,就通知这个挂在fill的消费者线程。就是在被动等(wait) 资源被filled了的意思
- 对于生产者来说,当cont == 1 也就是说此时有资源了,挂在 cont empty下,等待通知,消费者消费了一个,通知生产者线程,你需要生产了。在被动等(wait) 资源被消费(empted)没了的意思。
覆盖性条件
其实就是一个 cond 变量中 的条件模糊,覆盖范围多个线程都满足的情况亦或者条件都不满足,wait无法被唤醒。
cond_t c;
mutex_t m;
void * allocated(int size) {
pthread_mutex_lock(&m);
while(byteleft < size)
pthread_cond_wait(&c, &m);
void * ptr = ....; // get mem on heap
bytelfet -= size;
pthread_mutex_unlock(&m);
return ptr;
}
void free(void *ptr, int size) {
pthread_mutex_lock(&m);
byteleft+= size;
pthread_cond_signal(&c);// whom to signal?
pthread_mutex_unlock(&c);
}
比如一个线程Ta申请allocate(100), 另一个线程申请Tb allocate(10).此时可用内存为0.
那么两个线程就都睡了。
此时 Tc 线程 free(50),但是可能没有连续的空间,Tb 依然没有被唤醒(为什么没被唤醒呢?)。Ta也肯定没有被唤醒。此时大家都在睡觉。Tc线程不知道该唤醒睡。
解决方案 pthread_cond_broadcast(),唤醒所有线程,然后枪锁,最后大部分都回去继续sleep. 对性能影响很大,造成类似惊群的效果。大量的上下文切换。
一般只有在分配内存的情况下会调用broadcast.