5.3 多线程冲突了怎么办?
竞争与协作
多个线程如果竞争共享资源,如果不采取有效的措施,则会造成共享数据的混乱。
比如循环打印数字,出现数据不一致问题。
竞争条件(race condition)::多线程相互竞争操作共享变量时,在执行过程中发生了上下文切换,得到了错误的结果,且每次运行都可能得到不同的结果,因此输出的结果存在不确定性(indeterminate)。
临界区(critical section):它是访问共享资源的代码片段,一定不能给多线程同时执行。
互斥(mutualexclusion):也就说保证一个线程在临界区执行时,其他线程应该被阻止进入临界区
同步:就是并发进程/线程在一些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通信息称为进程/线程同步。
注意,同步与互斥是两种不同的概念:
- 同步就好比:「操作 A 应在操作 B 之前执行」,「操作 C 必须在操作 A 和操作 B 都完成之后才能执行」等;
- 互斥就好比:「操作 A 和操作 B 不能在同一时刻执行」;
互斥与同步的实现和使用
主要的方法有两种:
- 锁:加锁、解锁操作;
- 信号量:P、V 操作;
这两个都可以方便地实现进程/线程互斥,而信号量比锁的功能更强一些,它还可以方便地实现进程/线程同步。
锁
使用加锁操作和解锁操作可以解决并发线程/进程的互斥问题。
任何想进入临界区的线程,必须先执行加锁操作。若加锁操作顺利通过,则线程可进入临界区;在完成对临界资源的访问后再执行解锁操作,以释放该临界资源。
根据锁的实现不同,可以分为。
忙等待锁(自旋锁):获取不到锁时,线程就会一直 while 循环,不做任何事情
- 单CPU上需要抢占调度,因为在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU
无等待锁:当没获取到锁的时候,就把当前线程放入到锁的等待队列,然后执行调度程序,把 CPU 让给其他线程执行。
信号量
通常信号量表示资源的数量,对应的变量是一个整型(sem)变量。
另外,还有两个原子操作的系统调用函数来控制信号量的,分别是:
P 操作:将
sem减1,相减后,如果sem < 0,则进程/线程进入阻塞等待,否则继续,表明 P 操作可能会阻塞;- P 操作是用在进入临界区之前
V 操作:将
sem加1,相加后,如果sem >= 0,唤醒一个等待中的进程/线程,表明 V 操作不会阻塞;- V 操作是用在离开临界区之后
PV 操作的函数是由操作系统管理和实现的,所以操作系统已经使得执行 PV 函数时是具有原子性的。
想要实现互斥要为每类共享资源设置一个信号量
s,其初值为1如果互斥信号量为 1,表示没有线程进入临界区;
如果互斥信号量为 0,表示有一个线程进入临界区;
如果互斥信号量为 -1,表示一个线程进入临界区,另一个线程等待进入。
信号量实现事件同步,设置一个信号量,其初值为
0- 一个通过P操作之后等待其他的V操作之后执行
- V操作之后通知P操作执行
生产者 - 消费者问题
生产者 - 消费者问题描述:
- 生产者在生成数据后,放在一个缓冲区中;
- 消费者从缓冲区取出数据处理;
- 任何时刻,只能有一个生产者或消费者可以访问缓冲区;
我们对问题分析可以得出:
- 任何时刻只能有一个线程操作缓冲区,说明操作缓冲区是临界代码,需要互斥;
- 缓冲区空时,消费者必须等待生产者生成数据;缓冲区满时,生产者必须等待消费者取出数据。说明生产者和消费者需要同步。
那么我们需要三个信号量,分别是:
- 互斥信号量
mutex:用于互斥访问缓冲区,初始化值为 1; - 资源信号量
fullBuffers:用于消费者询问缓冲区是否有数据,有数据则读取数据,初始化值为 0(表明缓冲区一开始为空); - 资源信号量
emptyBuffers:用于生产者询问缓冲区是否有空位,有空位则生成数据,初始化值为 n(缓冲区大小);
经典同步问题
哲学家就餐问题

先来看看哲学家就餐的问题描述:
5个老大哥哲学家,闲着没事做,围绕着一张圆桌吃面;- 巧就巧在,这个桌子只有
5支叉子,每两个哲学家之间放一支叉子; - 哲学家围在一起先思考,思考中途饿了就会想进餐;
- 奇葩的是,这些哲学家要两支叉子才愿意吃面,也就是需要拿到左右两边的叉子才进餐;
- 吃完后,会把两支叉子放回原处,继续思考;
解法一
- 偶数位置先拿左边再拿右边,奇数位置先拿右边再拿左边,可以两人同时进餐。
解法二
- 每次只能有一个人能拿叉子,每次只能一个人进餐
解法三
- 每次每个人只有左右两侧都没有人拿叉子的时候立马拿起两个叉子,可以两人同时进餐
解法四
- 每个人先拿起右侧叉子再拿起左侧叉子,会出现死锁,满足四个条件
读者 - 写者问题
读写锁问题:通过读写互斥、写写互斥、读读兼容实现
