- 线程安全
- 什么是线程不安全
- 1.多个线程并行或者并发运行时,会导致结果的二义性
- 2.假设有两个线程,线程A和线程B有一个cpu.同时++
- 线程不安全的代码演示
- 例子:内存->寄存器->cpu->寄存器->内存,这个过程没有原子性,不能一步执行完,可能会被打断...存在二义性,所以就存在线程不安全.线程A刚拿到一个全局变量100,放在寄存器里.还没到写回内存这一步,被剥离处来了.线程B在内存里拿的话还是100,--之后是99,但是A运行时会在寄存器里拿出来100进行操作,就重复了.
- 解决线程不安全的场景-----互斥和同步
- 互斥:控制线程的访问时序,当多个线程能够同时访问到临界资源的时候,有可能导致二义性结果
- 互斥的原理
- 互斥锁:就是计数器;为1表示线程能获取到互斥锁,为0获取不到.一个线程拥有了互斥锁,即使被切换,别的线程也不能访问临界资源...0--->1或者1--->0的变化一定是原子性的
- 程序员在代码里用同一个互斥锁约束多个线程 (保证安全)
- 互斥锁的计数器为何能保证原子性
- 直接用一个寄存器里的值和内存中的计数器的值进行交换,交换的汇编指令,一步就能完成(初始的时候寄存器中的值为0),锁刚创建的时候是计数器是1
- 多个线程加同一个计数器,如果计数器值为1(说明锁资源空闲),1和0交换,寄存器变成0,加锁成功,进程占据锁资源,如果计数器为0(现在没有获取锁资源),0和0换,寄存器还是0,这个锁加不上去
- 解锁的时候寄存器里的值是加锁的时候换过来的1,和计数器里值为0的交换...锁只有一个.解锁是一定确定的
- 互斥锁的接口
- 动态初始化:第一个参数是一个pthread_mutex_t结构体类型的指针,在堆上开辟,传进去这个函数给你初始化..第二个参数NULL(采用默认属性)
- 静态初始化:将宏PTHREAD_MUTEX_INITIALIZER赋值给结构体对象
- 加锁接口1(互斥锁的阻塞加锁接口,拿不到锁就阻塞等待)()
- 加上锁之后如果线程退出,就不会释放锁...那么其他线程一直在等拿锁,直到主线程退出,死锁的线程才能退出.
- 加锁接口2(非阻塞加锁接口)
- 那到锁正常返回,拿不到锁,也直接返回,所以要搭配循环使用
- 加锁接口3
- 第二个参数是一个结构体,作用是等待拿锁的时间(等待期间拿到了锁,就返回),时间到了没拿到锁才返回.加锁成功返回0.不像try_lock拿不到锁直接返回.struct timespec{time_t tv_sec;(秒级)time_tv tv_nsec;(纳秒级)}
- 解锁接口
- mutex是互斥锁的变量的地址,解决上面加锁导致出现一个死锁的情况,线程A拿到了互斥锁,就会把全局变量减成0,循环退出后才会释放锁,这时候线程B才能拿到锁,拿到锁之后因为i已经到0了所以线程B直接退出啥也没干,多线程就没有意义了.(结果是程序能正常退出了,但是一个线程空闲)
- 能解锁了,但是有问题,例如打印1~10000的数字,只有一个线程工作,压根看不见另一个线程工作,就违背了,多线程的初衷
- 代码改成这样,(两个线程能交互运行了,但是主线程在等待两个线程,有一个程序又不退出了),break那里再加一个解锁就能退出了(结论:在线程所有可能退出的地方都加解锁,否则就有可能导致死锁)
- 同步
- 有了互斥之后为啥还要有同步
- 多线程保证了互斥,也就是保证了线程能独占访问临界资源,但是并不是说,各个线程在访问临界资源的时候都是合理的.同步就是为了保证多个线程对临界资源访问的合理性,这个合理性建立在多个线程互斥访问的基础上.
- 条件变量
- 条件变量的原理
- 线程在加锁之后,判断下临界资源是否可用:如果可用:直接访问临界资源如果不可用则调用等待接口,让该线程进行等待
- 本质上是PCB等待队列,将将需要等待某种资源的线程加入等待队列,等到资源到的时候出队列
- 条件变量的接口
- 初始化接口和销毁接口
- pthread_cond_t:条件变量的类型(就像锁一样用就行了),初始化一个条件变量,就是建立了一个等待队列
- 等待接口
- 谁调用等待接口就把谁加入条件变量对应的PCB等待队列中去...cond:是条件变量,第二个参数是互斥锁,加入等待队列后会把锁释放.从等待队列中出来后继续执行wait函数后面的代码
- 条件变量代码(两个版本)
- 第二个版本,弄四个线程(if换乘while,让他循环判断),但是变成while后程序退出不了,四个线程都被加到等待队列里了
- 解决这个问题:用boardcast其实行,但是不建议用,用两个等待队列解决
- 条件变量的重要问题
- 条件变量的等待队列中为啥要传互斥锁:传给pthread_cond_wait就是为让他释放锁
- 释放锁和加入等待队列的顺序:先放到等待队列,再释放锁.原因,如果线程A先释放锁,还没等A加入等待队列呢,线程B拿到锁之后操作完,唤醒等待队列的线程,但是A还没加进去,这情况就复杂了,如果B又一次拿到锁,再判断一下,条件不满足了,把自己也加进去了,俩全进去了,没人唤醒了.
- 唤醒线程之后在wait函数返回前会进行强锁操作,被唤醒的线程会执行wait函数后面的代码,抢锁也有抢到和抢不到的情况,如果抢到了,就执行wait函数后面的代码,如果没抢到,说明wait函数还没有执行完毕,还处于函数内部抢锁的逻辑,还会继续抢锁.
- 死锁
- 死锁的场景:1.线程没有释放锁就退出了或者线程加锁之后因为死循环或者其他原因没有释放.
- 死锁场景2:线程A拿着锁1,阻塞等待在想拿锁2,线程B拿着锁2,阻塞等待在想拿锁1.(两个线程分别拿着自己的锁,还想申请对方的锁)
- thread apply all bt (gdb调试里面让展示所有线程的调用堆栈)/t 线程序号 跳到某个线程的调用堆栈