目录
条件变量更深层次的理解(pthread_cond_wait内部原理)
问题引入:
- 但现象是,要么吃面线程一直吃面,要么做面线程一直做面,而不是,碗里没面做面线程往碗里做了一碗面,碗里有面吃面线程就把碗里的面吃了。
- 多个线程保证了互斥,也就是保证了线程能够独占访问临界资源了。但并不是说,各个线程在访问临界资源的时候都是合理的,同步是为了保证多个线程对临界资源的访问的合理性,这个合理性建立在多个线程保证互斥的情况下。
- 对资源加以判断后
- 结果
- 出现上面结果的原因
- 在单核CPU环境下,两个线程CPU拿到运行时,会给线程分配时间片,由于时间片远大于线程执行一次循环的时间,以吃面线程为例,假设碗里有面(g_bowl == 1),吃面线程抢到锁,吃面线程把面吃了(g_bowl - - ),然后解锁,可是吃面线程的时间片还没到,可以继续执行自己的代码,再次进入while循环,吃面线程再次抢到锁,可是此时碗里没面了(g_bowl == 0),它就会执行判断语句,打印内容,然后解锁,可是吃面线程的时间片还没到,再次进入while循环,可碗里还是没面(g_bowl == 0),再次执行判断语句,打印内容,然后解锁。。。。。。直到时间片用完,被剥离CPU,做面线程才有可能被调度,运行自己的代码,然后抢锁,往下执行。
- 当前写的这个代码,非常耗费CPU资源,因为判断了即使资源没有准备充分,释放掉互斥锁之后,也极有可能这个互斥锁又被该线程拿到。
条件变量:
- 条件变量的使用原理:
- 线程在加锁之后,判脚下临界资源是否可用:
- 如果可用:则直接访间临界资源
- 如果不可用:则调用等待接口,让该线程进行等待
- 线程在加锁之后,判脚下临界资源是否可用:
- 条件变量的原理
- 本质上是:PCB等待队列(存放在等待的线程的PCB)
条件变量接口:
- 初始化接口:
- 动态初始化
- int pthread _cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
- pthread_ cond_t :条件变量类型(一个结构体)
- 参数:
- cond
- 接收一个条件变量的指针(接收一个条件变量的地址)
- attr:
- 表示条件变量的属性信息,传递NULL,采用默认属性
- cond
- 条件变量的销毁
- int pthread_cond_destroy(pthread_cond_t *cond);
- 静态初始化:
- pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- 动态初始化
- 等待接口:
- int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
- 谁(线程)调用等待接口,就将谁放到条件变量对应的PCB等待队列当中
- 参数:
- cond :条件变量
- mutex:互斥锁
- int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
- 唤醒接口:
- int pthread_cond_broadcast(pthread_cond_t *cong);
- 唤醒PCB等待队列当中的所有线程
- int pthread_cond_signal(pthread_cond_t *cond),
- 唤醒PCB等待都一列当中至少一个线程
- int pthread_cond_broadcast(pthread_cond_t *cong);
- 吃面线程和做面线程使用条件条件变量后:
- 终于达到了预想的结果,吃面线程吃一碗面,做面线程做一碗面
- 但是,增加吃面线程和做面线程的个数后:
- 2个吃面线程,2个做面线程
- 结果是要么某个吃面线程一直吃面,要么某个做面线程一直做面,
- 这个问题先放一放,再对条件变量往深层次理解
条件变量更深层次的理解(pthread_cond_wait内部原理)
- 1.条件变量的等待接口第二个参数为什么会有互斥锁?
- 这个问题用一个吃面线程和一个做面线程做假设
- 假设吃面线程先拿到互斥锁,且g _bowl == 0(碗里没面),假设在调用函数pthread_cond_wait()之后,函数内部没有解锁逻辑,则当吃面线程被放到条件变量对应的PCB等待队列中时,互斥锁也被它带走了,那么做面线程永远也拿不到锁,就会一直阻塞在加锁接口,无法往下执行,也就不会++g_bowl,更不会通知吃面线程吃面,这样就会导致整个进程卡死了,因此,在函数pthread_cond_wait()内部,会有解锁的逻辑。
- 1.在线程访问临界资源之前,一定是加锁访问的,保证互斥属性。2.传递互斥锁给pthread_cond_wait()接口,就是想让当前线程放到条件变量对应的PCB等待队列中时可以进行解锁。
- 2.ptrhead_cond_wait的内部是先释放互斥锁还是先将线程放入到PCB等待队列?
- 这个问题还是用一个吃面线程和一个做面线程做假设
- 假设吃面线程先拿到互斥锁,且g _bowl == 0(碗里没面),然后就会调用函数pthread_cond_wait(),假设是先解锁,再将线程放入到条件变量对应的PCB等待队列中,则吃面线程刚解完锁,做面线程就立即抢到锁,然后就++g_bowl,并通知等待队列当中的吃面线程来吃面,但是吃面线程还没有被放到条件变量对应的PCB等待队列中,也就意味着,做面线程去通知条件变量对应的PCB等待队列的时候,该队列还是一个空队列,当吃面被放到条件变量对应的PCB等待队列中时,做面线程再次拿到锁,然后判断,发现g_bowl == 1,他也就被放到条件变量对应的PCB等待队列中,此时,两个线程在条件变量对应的PCB等待队列中,就再也没有线程来通知他们,就会导致整个进程卡死。因此,假设不成立。
- 结论:先放到PCB等待队列,再进行解锁
- 这个问题还是用一个吃面线程和一个做面线程做假设
- 3.线程被唤醒了之后,需要再获取互斥锁吗?
- pthread_cond_wait函数在返回之前一定会在其内部进行加锁操作;
- 如果pthread_cond_wait函数在返回之前没有加锁操作,那么pthread_cond_wait函数在返回之后,该线程就会存有在不加锁的情况下访问临界资源,也就是不保证互斥访问,就会造成线程不安全现象。因此pthread_cond_wait函数在返回之前一定会在其内部进行加锁操作。
- 抢锁的时候:
- 1.抢到了,pthread_cond_wait函数就真正执行完毕了,函数返回。
- 2.没抢到,pthread_cond_wait函数的代码就没有真正的执行完毕,还处于函数内部抢锁的逻辑当中,还会继续去抢锁,直到抢到互斥锁,才返回。
- 现在可以解决上面遗留的问题了(画图不是太方面,就以文字来描述一下)
- 现在有4个线程,吃面线程1,吃面线程2,做面线程1,做面线程2,假设吃面线程1先拿到互斥锁,且g _bowl == 0(碗里没面),调用函数pthread_cond_wait()后,先放到PCB等待队列,再进行解锁,此时其他三个线程都有可能会抢到锁,假设吃面线程2先拿到互斥锁,也会被放到PCB等待队列,然后解锁,此时枪锁的只有另外两个做面线程,假设做面线程1先抢到锁,它发现g_bowl == 0(碗里没面),他就会++g_bowl(做一碗面),然后解锁,然后通知等待队列中的两个吃面线程,它一通知,就有可能把两个吃面线程从等待队列中通知出来,假设此时吃面线程1先抢到互斥锁,pthread_cond_wait()函数返回,按照代码逻辑,吃面线程1就会往下执行,g_bowl--(吃面),此时g_bowl == 0,然后解锁,刚解锁,被通知出来吃面的吃面线程2就立即抢到锁,pthread_cond_wait()函数返回,按照代码逻辑,吃面线程2就会往下执行,g_bowl--(吃面),此时g_bowl == -1,然后解锁,刚解锁,如果又被吃面线程中的某一个抢到锁,由于此时g_bowl已经小于0,不会再执行判断语句,然后就g_bowl--(吃面),解锁,又被吃面线程中的某一个抢到锁,就这样,会在结果中看到负数,而看到很大的正数的场景和这个类似,就不再展开。
- 如何解决呢:
- 当pthread_cond_wait()返回之后,再次判断临界资源是否可用
- 当pthread_cond_wait()返回之后,再次判断临界资源是否可用
- 但是,这又引入了新的问题:
- 改进后的运行结果如下:
- 查看调用堆栈后发现:4个工作线程都阻塞在pthread_cond_wait()接口
- 原因分析:4个工作线程最终都被放到了条件变量对应的PCB等待队列中,再没有线程去唤醒PCB等待队列中的4个线程
- 对出现这种场景做一个详细分析:现在有4个线程,吃面线程1,吃面线程2,做面线程1,做面线程2,假设吃面线程1先拿到互斥锁,且g _bowl == 0(碗里没面),调用函数pthread_cond_wait()后,先将自己放到PCB等待队列,然后解锁,此时其他三个线程都有可能会抢到锁,假设吃面线程2先抢到互斥锁,也会被放到PCB等待队列,然后解锁,此时抢锁的只有另外两个做面线程,假设做面线程1先抢到锁,它发现g_bowl == 0(碗里没面),他就会++g_bowl(做一碗面),g_bowl由0变为1,然后解锁,然后去唤醒等待队列中的吃面线程,然而这次它只唤醒了其中的一个吃面线程,假设唤醒的是吃面线程2,但是吃面线程2的pthread_cond_wait()还没有真正返回,还在抢锁阶段,就在这个时候,锁被做面线程2抢到了,它访问临界资源,发现g_bowl == 1,就会调用函数pthread_cond_wait(),将自己放到PCB等待队列,然后解锁,可是被唤醒的吃面线程2还是没有抢到锁,锁被做面线程1先抢到了,它访问临界资源,发现g_bowl == 1,就会调用函数pthread_cond_wait(),将自己放到PCB等待队列,然后解锁,现在,被唤醒的吃面线程2终于抢到锁了,它访问临界资源,发现g_bowl == 1,可以使用,然后就g_bowl--(吃面),g_bowl由1变为0,然后解锁,然后去唤醒等待队列中的3个线程,但是它只唤醒了3个线程中的吃面线程1,另外2个做面线程没有被唤醒,被唤醒的吃面线程1拿到锁,访问临界资源,发现g_bowl == 0,临界资源不可用,就会调用函数pthread_cond_wait(),将自己放到PCB等待队列,然后解锁,此时能拿到锁的只有吃面线程2,他拿到锁,访问临界资源,发现g_bowl == 0,临界资源不可用,就会调用函数pthread_cond_wait(),将自己放到PCB等待队列,然后解锁,此时,4个线程都被放到了PCB等待队列,再没有线程去唤醒他们。
- 解决方案1:分别给吃面线程和做面线程创建相应的条件变量
- 吃面线程有自己的条件变量,做面线程有自己的条件变量
- 吃面线程发现g _bowl == 0(碗里没面),就将自己放到吃面线程的条件变量对应的PCB等待队列当中。
- 当吃面线程吃了一碗面,则通知做面线程的条件变量的PCB等待队列。
- 做面线程发现g _bowl == 1(碗里有面),就将自己放到做面线程的条件变量对应的PCB等待队列当中。
- 当做面线程做了一碗面,则通知吃面线程的条件变量的PCB等待队列。
- 解决方案2:
- 唤醒接口使用pthread_cond_broadcast(pthread_cond_t *cong),唤醒PCB等待队列当中的所有线程,但这样会浪费CPU资源,不推荐
- 吃面线程和做面线程终极版本:
- 运行结果: