互斥
Why?
多线程并发执行,对相同的数据做操作会产生错误的结果,例如两个人修改同一个数,可能会丢失一个修改。
对相同数据的操作在操作系统分为三步:读取、计算、写回。之间不能让别人来插一脚。
What?
- 竞争条件(race condition):多线程相互竞争操作共享变量时输出的结果存在不确定性(indeterminate)
- 临界区(critical section):访问共享资源的代码片段,不能多线程执行
- 互斥(mutualexclusion):保证一个线程在临界区执行时,其他线程应该被阻止进入临界区
- 同步:并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步。
锁
- 进入临界区前申请加锁,锁只能被锁一次,解锁后才能再锁
- 退出临界区时解锁
- 忙等待锁、自旋锁(spin lock):得不到锁就for循环一直获取锁
- 无等待锁:得不到锁放入锁等待队列,让出时间片
信号量
-
信号量:一个整数(sem),表示资源的数量
-
P 操作:将 sem 减 1,减后如果 sem < 0,则进程/线程进入阻塞等待队列
-
V 操作:将 sem 加 1,相加后,如果 sem <= 0,唤醒一个等待中的进程/线程,表明 V 操作不会阻塞
-
PV 操作的函数是由操作系统管理和实现的,具有原子性。
生产消费问题
- 生产者在生成数据后,放在一个缓冲区中;
- 消费者从缓冲区取出数据处理;
- 任何时刻,只能有一个生产者或消费者可以访问缓冲区;
- 互斥信号量 mutex:用于互斥访问缓冲区,初始化值为 1;
- 资源信号量 fullBuffers:用于消费者询问缓冲区是否有数据,有数据则读取数据,初始化值为 0(表明缓冲区一开始为空);
- 资源信号量 emptyBuffers:用于生产者询问缓冲区是否有空位,有空位则生成数据,初始化值为 n (缓冲区大小);
哲学家就餐问题
-
五人在圆形的桌子上吃饭,每人之间有一只筷子
-
解决办法:
- 避免死锁:偶数编号的哲学家「先拿左边的叉子后拿右边的叉子」,奇数编号的哲学家「先拿右边的叉子后拿左边的叉子」
- 最高效率:同时刻左右两个人吃饭且间隔,轮流来吃。
- 先拿左筷子,在拿右筷子,没有右筷子就放下左筷子。
读者写者问题
-
读读不互斥,读写互斥、写写互斥
-
读写优先级分为:
- 读优先:写等待时,允许读插队
- 写优先:有写等待,新来的读就阻塞。所有写完才能读。
- 公平策略:读写等价,有读等待写就排队,有写等待读就排队
各种锁
Why?
避免多线程访问共享资源产生的冲突
How?
加锁保证共享资源在任意时间里,只有一个线程访问避免多线程导致共享数据错乱的问题。
-
悲观锁:多线程同时修改共享资源的概率比较高,必须先上锁
-
互斥锁:加锁失败后,线程会释放 CPU ,给其他线程;
- 互斥锁加锁失败时,会从用户态陷入到内核态,开销大(两次线程上下文切换的成本)。
- 不适合锁时间特别短的
-
自旋锁:加锁失败后,线程会忙等待,直到它拿到锁;
- CAS 函数(Compare And Swap),在「用户态」完成加锁和解锁操作,无上下文切换
- 适合异步、协程等在用户态切换请求的编程方式,不适合锁时间长的
- 单核 CPU 上,需要抢占式的调度器
-
读写锁:写锁独占
-
适合读多写少
-
读优先、写优先:产生饥饿
-
公平读写锁:用队列把获取锁的线程排队,不允许插队
-
-
-
乐观锁:先修改完共享资源,再验证这段时间内有没有发生冲突,如果没有其他线程在修改资源,那么操作完成,如果发现有其他线程已经修改过这个资源,就放弃本次操作。
- 共享文档、SVN 和 Git
- 在冲突概率非常低,且加锁成本非常高的场景时,才考虑使用乐观锁。
死锁
-
死锁:线程相互等待着对方持有的锁,谁也不能执行
-
四个条件:
- 互斥:多个线程不能同时使用同一个资源
- 持有并等待:线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1
- 不可剥夺:自己使用完之前不能被其他线程获取
- 环路等待:线程获取资源的顺序构成了环形链
避免死锁
只需要破环其中一个条件就可以。
- 资源有序分配法:多线程获取资源的顺序相同:abcd这样的顺序