多线程同步

多线程同步

  • 一、竞争与协作
    • 互斥
    • 同步
    • 互斥与同步
  • 二、互斥与同步的使用
    • 信号量
  • 三、经典问题
    • 生产者-消费者问题
    • 哲学者就餐问题
      • 方案一:有叉子就用
      • 方案二:只准一个人拿叉子
      • 方案三:规定先拿的叉子
      • 方案四:记录状态
    • 读写者问题
      • 方案一:读者优先的策略
      • 方案二:写者优先策略
      • 方案三:公平策略

一、竞争与协作

互斥

由于多线程执行操作共享变量的这段代码可能会导致竞争状态,因此我们将此段代码称为临界区,它是访问共享资源的代码片段,⼀定不能给多线程同时执行。所以我们希望这段代码是互斥的,也就说保证⼀个线程在临界区执行时,其他线程应该被组织进入临界区。

同步

所谓同步,就是并发进程/线程在⼀些关键点上可能需要互相等待与互通消息,这种相互制约的等待与互通 信息称为进程/线程同步。

互斥与同步

  • 互斥:操作A和操作B不能在同⼀时刻执行
  • 同步:操作A应在操作B之前执行,操作C必须在操作A和操作B都完成之后才能执行。

二、互斥与同步的使用

为了实现进程/线程间正确的协作,操作系统必须提供实现进程协作的措施和方法,主要的方法有两种:

  • 锁:加锁、解锁操作
  • 信号量:P、V 操作

这两个都可以方便地实现进程/线程互斥,而信号量比锁的功能更强⼀些,它还可以方便地实现进程/线程同步。

任何想进⼊临界区的线程,必须先执⾏加锁操作。若加锁操作顺利通过,则线程可进⼊临界区;在完成后,对临界资源的访问后再执行解锁操作,以释放该临界资源。
根据锁的实现不同,可以分为忙等待锁和无忙等待锁。

  • 忙等待锁:当获取不到锁时,线程就会⼀直wile循环,不做任何事情,所以就被称为忙等待锁,也被称为自旋锁。 这是最简单的⼀种锁,⼀直自旋,利用CPU 周期,直到锁可用。在单处理器上,需要抢占式的调度器(即不断通过时钟中断⼀个线程,运行其他线程)。否则,自旋锁在单 CPU上无法使用,因为⼀个自旋的线程永远不会放弃CPU。
  • 无等待锁:等待锁顾明思议就是获取不到锁的时候,不用自旋。
    既然不想自旋,那当没获取到锁的时候,就把当前线程放入到锁的等待队列,然后执行调度程序,把CPU让给其他线程执行。

信号量

信号量是操作系统提供的⼀种协调共享资源访问的方法。 通常信号量表示资源的数量,对应的变量是⼀个整型( sem )变量。 另外,还有两个原子操作的系统调用函数来控制信号量的,分别是:

  • P 操作:将 sem 减 1 ,相减后,如果 sem < 0 ,则进程/线程进⼊阻塞等待,否则继续,表明 P 操作可能会阻塞;
  • V 操作:将 sem 加 1 ,相加后,如果 sem <= 0 ,唤醒⼀个等待中的进程/线程,表明 V 操作不会阻塞;

P 操作是用在进⼊临界区之前,V 操作是⽤在离开临界区之后,这两个操作是必须成对出现的。

PV 操作的函数是由操作系统管理和实现的,所以操作系统已经使得执行PV 函数时是具有原子性的。
信号量不仅可以实现临界区的互斥访问控制,还可以线程间的事件同步。
我们先来说说如何使用信号量实现临界区的互斥访问。
对于两个并发线程,互斥信号量的值仅取 1、0 和 -1 三个值,分别表示

  • 如果互斥信号量为 1,表示没有线程进⼊临界区;
  • 如果互斥信号量为 0,表示有⼀个线程进⼊临界区;
  • 如果互斥信号量为 -1,表示⼀个线程进⼊临界区,另⼀个线程等待进⼊。

同步的方式是设置⼀个信号量,其初值为 0。

三、经典问题

生产者-消费者问题

生产者-消费者问题描述:

  • 生产者在生成数据后,放在⼀个缓冲区中
  • 消费者从缓冲区取出数据处理
  • 任何时刻,只能有⼀个生产者或消费者可以访问缓冲区

那么我们需要三个信号量,分别是

  • 互斥信号量 mutex :用于互斥访问缓冲区,初始化值为 1
  • 资源信号量 fullBuffers :用于消费者询问缓冲区是否有数据,有数据则读取数据,初始化值为 0 (表明缓冲区⼀开始为空)
  • 资源信号量 emptyBuffers :用于生产者询问缓冲区是否有空位,有空位则生成数据,初始化值为 n (缓冲区大小)

如果消费者线程⼀开始执行P(fullBuffers) ,由于信号量 fullBuffers初始值为 0,则此时fullBuffers的值从0变为 -1,说明缓冲区里没有数据,消费者只能等待。接着,轮到生产者执行P(emptyBuffers) ,表示减少1个空槽,如果当前没有其他生产者线程在临界区执行代码,那么该生产者线程就可以把数据放到缓冲区,放完后,执行V(fullBuffers) ,信号量fullBuffers从-1变成0,表明有消费者线程正在阻塞等待数据,于是阻塞等待的消费者线程会被唤醒。 消费者线程被唤醒后,如果此时没有其他消费者线程在读数据,那么就可以直接进入临界区,从缓冲区读取数据。最后,离开临界区后,把空槽的个数 + 1。

哲学者就餐问题

先来看看哲学家就餐的问题描述

  • 5个老大哥哲学家,闲着没事做,围绕着⼀张圆桌吃面
  • 巧就巧在,这个桌子只有5支叉子,每两个哲学家之间放一支叉子
  • 哲学家围在⼀起先思考,思考中途饿了就会想进餐
  • 奇葩的是,这些哲学家要两支叉子才愿意吃面,也就是需要拿到左右两边的叉子才进餐
  • 吃完后,会把两支叉子放回原处,继续思考

方案一:有叉子就用

假设五位哲学家同时拿起左边的叉子,桌面上就没有叉子了, 这样就没有人能够拿到他们右边的叉子,很明显这发生了死锁的现象。

方案二:只准一个人拿叉子

只要有一个哲学家进了临界区,也就是准备要拿叉子时, 其他哲学家都不能动,只有这位哲学家用完叉子了,才能轮到下一个哲学家进餐。

方案三:规定先拿的叉子

让偶数编号的哲学家先拿左边的叉子后拿右边的叉子,奇数编号的哲学家先拿右边的叉子后拿左边的叉子。

方案四:记录状态

我们用⼀个数组state来记录每⼀位哲学家在进程、思考还是饥饿状态(正在试图拿叉子)。 那么,一个哲学家只有在两个邻居都没有进餐时,才可以进入进餐状态。

读写者问题

读者-写者的问题描述:

  • 读-读允许:同⼀时刻,允许多个读者同时读
  • 读-写互斥:没有写者时读者才能读,没有读者时写者才能写
  • 写-写互斥:没有其他写者时,写者才能写

方案一:读者优先的策略

  • 信号量 wMutex :控制写操作的互斥信号量,初始值为 1
  • 读者计数 rCount :正在进行读操作的读者个数,初始化为 0
  • 信号量 rCountMutex :控制对 rCount 读者计数器的互斥修改,初始值为 1

是读者优先的策略,因为只要有读者正在读的状态,后来的读者都可以直接进入,如果读者持续不断进⼊,则写者会处于饥饿状态。

方案二:写者优先策略

  • 信号量 rMutex :控制读者进⼊的互斥信号量,初始值为 1
  • 信号量 wDataMutex :控制写者写操作的互斥信号量,初始值为 1
  • 写者计数 wCount :记录写者数量,初始值为 0
  • 信号量 wCountMutex :控制 wCount 互斥修改,初始值为 1

方案三:公平策略

  • 优先级相同
  • 写者、读者互斥访问
  • 只能⼀个写者访问临界区
  • 可以有多个读者同时访问临界资源

你可能感兴趣的:(图解系统,多线程同步,锁,信号量,生产者-消费者问题,哲学家问题)