文章目录
线程同步
同步的引入
如果一个共享资源只加了锁,就有可能出现锁的饥饿问题
例:
一个死循环抢票的代码
while(1)
{
//加锁
p–
//解锁
}
一个线程a抢到锁之后,其他线程想要锁的进程就只能阻塞等待线程a解锁
线程a使用完临界区之后,解锁之后,又进入下一次循环,又去抢锁了
因为其他想抢锁的线程还阻塞着,唤醒需要时间
但是线程a本来就醒着,所以线程a就比别的线程快,马上又把锁抢到了
其他想要锁的线程只能再次进入阻塞状态
就有可能一直是线程a拿着锁,访问临界区
怎么解决这个问题?
就要用到同步了
即:
①规定所有没有获取到锁的,又想要锁的,
阻塞时必须排队
②获取了锁的线程,执行完临界区代码并解锁之后,即使还要想获取锁,
也必须排在队伍最末尾
同步和互斥存在的意义
互斥是为了保护共享资源,防止出现数据不一致问题
但是互斥并不能
保证临界区代码高效合理
同步则是作为互斥的补充
同步保证多执行流时,执行流在访问共享资源时具有一定的顺序性
同步是在互斥的基础(共享资源安全)之上,保障临界区代码的高效合理
条件变量的理解
举例子来理解:
有n+1个人闲的没事干,一起玩"拿苹果和放苹果"的小游戏
游戏规则:
①一个人负责往盘子里面放苹果
②n个人蒙上眼睛,从盘子里面拿苹果
③如果盘子里面有苹果,那放苹果的人就不会再放了
④如果盘子里面没苹果,那么拿苹果的人可能再次检测盘子看它是否有苹果
⑤盘子被加了互斥锁,即同时只能有一个人去使用盘子
如果没加同步
那么就会和引入时所说的,出现饥饿问题,即可能有一个蒙着眼睛的人没拿到苹果,就一直用手去摸盘子
导致放苹果的人放不了,就出现锁的饥饿问题了
所以他们商量了一下,把规则改成了:
①一个
蒙眼
拿苹果的人如果没有拿到苹果,就不能再用手去摸盘子了,必须去等待队列中阻塞等待
即:条件不具备,就阻塞
②如果蒙眼的人都去阻塞等了,那就没人来拿苹果了呀?
所以给放苹果的人一个铃铛,如果他往盘子里面放了苹果,那么就敲一下铃铛
此时等待队列中的第一个人才可以去拿苹果
即:条件具备了,就唤醒
③拿到苹果的人,如果还想拿苹果,就得排到等待队列末尾
所以
如果我们把
- ①所有的人都是线程
- ②锁就是锁
- ③盘子就是临界资源
- ④苹果就是数据
- ⑤铃铛+等待队列就是条件变量
- ⑥让蒙眼的人去等待队列等这个操作就是
pthread_cond_wait
- ⑦敲一下铃铛这个操作就是
pthread_cond_signal
- ⑧敲n下铃铛就是
pthread_cond_broadcast
所以条件变量是:
条件变量是一个用来实现线程同步的特性,内部会维护一个等待队列
相当与是一个:提示器+等待队列
条件变量相关接口
条件变量的结构体
pthread_cond_t
类型的结构体
分为
①局部条件变量[在局部定义的条件变量]
- 1.只能使用
pthread_cond_init
初始化 - 2.并且需要使用
pthread_cond_destroy
销毁局部条件变量 - 3.条件变量是局部的,所以要让所有线程都看到的话,就需要把条件变量的地址/引用传给所有线程
- 1.只能使用
②全局条件变量[在全局定义的条件变量]
全局条件变量可以使用pthread_cond_init
或者宏PTHREAD_COND_INITIALIZER
初始化
全局条件变量销不销毁无所谓,因为生命周期本来就和进程一样长
库函数:pthread_cond_init
头文件:
pthread.h
参数表:
①
pthread_cond_t*mu
:要初始化的条件变量的地址②
const pthread_condattr_t*
:用户指定的条件变量的属性,一般不管,设置为nullptr作用:
初始化对应的条件变量
库函数:pthread_cond_destroy
头文件:
pthread.h
参数表:
pthread_cond_t*mu
:要销毁的条件变量的地址作用:
销毁对应的条件变量
库函数:pthread_cond_wait
头文件:
pthread.h
参数表:
- ①
pthread_cond_t*comd
:指定的条件变量的地址 - ②
pthread_mutex_t*mu
:指定的互斥锁的地址
- ①
作用:
使用指定的互斥锁保障线程安全,并让调用该函数的线程,在指定的条件变量的等待队列中阻塞等待注意:
①线程执行
pthread_cond_wait
函数时,在进入条件变量的等待队列之前,会让线程解一次锁(解的是传进来的锁)
,不然拿着锁去阻塞了,就很可能会死锁②当条件变量的等待队列中的线程被唤醒时,线程继续执行
pthread_cond_wait
中剩余的代码时,因为出了wait函数就是临界区,所以必须再申请一次锁(申请的是传进来的锁)才能出pthread_cond_wait
函数,互斥地进入临界区
此时如果锁被其他线程拿走了,线程就又会阻塞,只不过此时线程是在锁的等待队列中阻塞的,而不是条件变量的等待队列
库函数:pthread_cond_signal
头文件:
pthread.h
参数表:
pthread_cond_t*cond
:指定的条件变量的地址作用:
唤醒指定条件变量的等待队列中的队头线程(第一个线程)
为什么唤醒一般是唤醒队头线程?而不是随机唤醒?
①唤醒队头线程的
时间复杂度一般情况下比随机唤醒低
而且唤醒队头线程确定性更高,更好调试②在等待队列队头的线程,可以理解为最先进入等待队列的线程,即等待时间最长的线程
为了防止饥饿,它理应被最先唤醒
库函数:pthread_cond_broadcast
头文件:
pthread.h
参数表:
pthread_cond_t*cond
:指定的条件变量的地址作用:
唤醒指定条件变量的等待队列中的所有线程
条件变量的使用要点
线程一定是在临界区中进入条件变量的等待队列的
1.因为条件变量本身也是共享资源,也需要被保护
即:需要使用锁保证条件变量的wait函数具有“原子性”,pthread_cond_wait
需要原子性的释放锁并进入等待队列等待
不然调用pthread_cond_wait
都有线程安全问题2.因为进入条件变量的等待队列是有条件的,又因为条件变量是实现多线程间同步的
所以这多个线程要满足的条件(看到的条件)是一样的,即条件本身就是共享资源/共享资源的一部分
即判断这个条件是否成立,一定需要访问共享资源
所以线程调用pthread_cond_wait
的时候,一定是拿着锁的
所以为了防止拿着锁的线程,去条件变量的等待队列下等待,造成死锁
所以:1.
pthread_cond_wait
才会在自己的函数体中先解锁,再把线程放进等待队列,这个是为了防止死锁2.
pthread_cond_wait
才会在线程被唤醒之后,再让线程申请一次锁,这个是为了恢复等待之前线程持有锁的状态[因为线程从条件变量的等待队列中醒来时,还在临界区中
]
3.条件变量的wait函数的实现,就会释放锁和申请锁
所以:保护共享的条件的时候,必须得用锁!没法用其他保护手段
可以通过条件变量控制线程运行的顺序
在一次创建了多个线程之后,线程的调度顺序如何我们并不清楚,因为这完全是由操作系统自主决定的
但是我们可以通过让线程满足某些条件时,进入条件变量的等待队列
以及满足某些条件时,唤醒条件变量的等待队列中的线程
一定程度上做到操纵线程的调用顺序
伪唤醒问题的解决
即:判断线程是否要进入条件变量的等待队列时,判断不能用if而要用while
不然就有可能出现伪唤醒问题:即在条件变量下等待的线程,唤醒条件其实并不满足,但是因为程序员编码的问题,可能意外被唤醒了
例如:
生产者消费者模型中,因为阻塞队列中没有数据,所以全部都5个消费者线程在条件变量的等待队列中等待
生产者线程生产了一个数据,意外地把唤醒了多个消费者线程
然后一个消费者线程抢到锁之后,把阻塞队列中那唯一的一个数据抢走了,它解锁之后
因为唤醒了多个消费者线程
所以锁可能又被一个消费者线程抢到了,但是此时阻塞队列中根本没有数据!
此时:
1.如果此时是使用if进行“
线程是否需要进入条件变量的等待队列
"的判断的这个被伪唤醒的线程,重新申请并拿到锁之后,就直接"饿虎出笼"去肆意妄为了2.如果是使用while进行“
线程是否需要进入条件变量的等待队列
”的判断的,这个被唤醒的线程,重新申请并拿到锁之后,也还是不能直接出循环,因为要再判断一下循环条件是否不满足了
虽然循环条件是"线程需要进入等待队列"的条件,所以如果这个条件满足,不就意味着线程不应该被唤醒吗?