操作系统10----并发与同步2

it2022-05-05  125

                                                        并发与同步2

1.信号量(semaphore)

2.管程(Moniter)

3.条件变量(Condition Variable)

4.哲学家就餐问题

5.读者-写者问题

5.1读者优先----用信号量解决读者-写者问题

5.2写者优先----用管程解决读者-写者问题


并发问题:多线程并发导致资源竞争

同步概念:协调多线程对共享数据的访问;任何时刻只能有一个线程执行临界区代码;

基本同步方法

最底层需要硬件支持,例如禁用中断,原子操作等;在此之上进行抽象,产生信号量,锁以及条件变量等概念;进而支持临界区和管程等高级抽象的实现;


1.信号量(semaphore)

信号量是操作系统提供的一种协调共享资源访问的方法,由Dijkstra20世纪60年代提出。

信号是一种抽象数据类型,由一个整形 (sem)变量和两个原子操作组成:

P() sem减1,如sem<0, 进入等待, 否则继续;

V() sem加1,如sem≤0,唤醒一个等待进程;

信号量的特性

信号量是被保护整数变量,初始化完成后,只能通过P()V()操作修改,由操作系统保证,PV操作是原子操作;

P() 可能阻塞V()不会阻塞;

通常假定信号量是“公平的”,线程不会被无限期阻塞在P()操作,假定信号量等待按先进先出排队;

信号量的实现

信号量主要由整形变量sem和等待队列组成,

对于P操作,sem--,如果当前sem<0,则该线程加入到等待队列,阻塞;

对于V操作,sem++,如果当前sem<=0,则说明当前由线程在等待队里阻塞等待,则唤醒其中一个线程执行;

信号量分类

二进制信号量:资源数目为01

资源信号量:资源数目为任何非负值

信号量的使用

互斥访问:临界区的互斥访问控制

条件同步:线程间的事件等待

用信号量实现临界区的互斥访问

每个临界区设置一个信号量,其初值为1,进入临界区之前对于信号量执行P操作,离开临界区时对于信号量执行V操作;

用信号量实现条件同步

每个条件同步设置一个信号量,其初值为0,由于信号量初始为零,因此线程A执行P操作会阻塞,只有等到线程B执行完V操作之后,线程A才会继续执行,即让线程A等待线程B的执行,实现同步。

用信号量解决生产者-消费者问题

任何时刻只能有一个线程操作缓冲区,缓冲区空时,消费者必须等待生产者,缓冲区满时,生产者必须等待消费者。

mutex为二进制信号量,用于缓冲区的互斥访问,

fullBuffers为缓冲区满信号量,emptyBuffers为缓冲区空信号量,用于条件同步

对于生产者,首先判断缓冲区是否有空闲,如果非空,则不能生成需要等待;如果缓冲区有空闲,则对于缓冲区加锁,生产一个元素,释放信号量mutex,此时对于fullBuffers执行V操作,说明当前缓冲区中有元素,如果有消费者等待,则可以执行消费操作。

对于消费者,首先判断缓冲区是否有元素,如果没有,则需要阻塞等待;如果缓冲区有元素,则对于缓冲区加锁,消费元素,释放锁;对于emptyBuffers执行V操作,如果此时有生产者正在等待生产,则可以继续生产,实现同步。


2.管程(Moniter

管程是一种用于多线程互斥访问共享资源的程序结构,采用面向对象方法,简化了线程间的同步控制,任一时刻最多只有一个线程执行管程代码,正在管程中的线程可临时放弃管程的互斥访问,等待事件出现时恢复。

管程的组成

一个锁:控制管程代码的互斥访问

0或者多个条件变量:管理共享数据的并发访问


3.条件变量(Condition Variable

条件变量是管程内的等待机制,进入管程的线程因资源被占用而进入等待状态,每个条件变量表示一种等待原因,对应一个等待队列。

Wait()操作 将自己阻塞在等待队列中,唤醒一个等待者或释放管程的互斥访问;

Signal()操作  将等待队列中的一个线程唤醒,如果等待队列为空,则等同空操作;

条件变量实现

条件变量由等待队列和numwaiting组成。

注意条件变量是对于锁来使用的,在执行wait操作之前,首先需要获取锁。

对于wait操作,等待数量加一,将线程加入到等待队列,然后释放锁,挑选另一个线程执行,再次加锁;

对于signal操作,如果当前有等待线程,则挑选一个等待线程,唤醒来执行,执行等待数量减一;

用管程解决生产者-消费者问题

一个锁用来互斥,两个条件变量,用来同步。

对于生产者,首先获取锁,判断缓冲区是否已满,如果已满,则执行等待,此时当前线程加入到等待队列,释放锁,操作系统执行其他线程;如果当前缓冲区非满,则生产一个元素,通知消费者,可以进行消费,最后释放锁。

对于消费者,首先获取锁,判断缓冲区是否已空,如果已空,则执行等待;如果非空,则消费一个元素,通知生产者可以进行生产。

管程条件变量的释放处理方式

   

对于管程来说,某一时刻,T1位于管程中,获取锁,执行wait操作,释放管程锁,进入等待过程;

T2进入管程,执行signal操作,此时T2在管程中,执行singal操作之后,T1会被唤醒,为避免管程中同时存在两个线程,因此需要控制T1恢复执行的时机,hansen方法是T2执行完毕,离开管程,T1才开始在管程中继续执行;hoare方法则T1立即恢复执行执行完毕之后,T2继续执行。hansen办法高效,hoare办法需要多执行一次上下文切换,比较低效。

Hansen管程:条件变量释放仅是一个提示,需要重新检查条件

Hoare管程:条件变量释放同时表示放弃管程访问,释放后条件变量的状态可用


4.哲学家就餐问题

方案一:

将叉子作为共享资源,利用5个信号量来表示。

哲学家编号为0--4,对于每个哲学家来说,首先思考,然后拿左右叉子,然后吃饭,再放下左右叉子。

该方案容易出现死锁问题,例如5位哲学家同时拿起左边叉子,下一步会同时等待拿右边叉子,此时会出现死锁问题。

方案二:

设置5个信号量来代表叉子资源,同时设置一个二值信号量来作为互斥锁,用来保证同时只有一位哲学家可以吃饭。

每位哲学家思考之后,同时来竞争互斥锁,只有一位哲学家可以吃饭,可以保证并发问题,但是效率不高。

方案三:

可以多人就餐

方案四:

指导原则;要么不拿,要么拿两把叉子

1.思考中

2.进入饥饿状态

3.如果左邻居或右邻居正在进餐,等待;否则,转4

4.拿起两把叉子

5.吃饭

6.放下左叉子  放下左叉子,可能要唤醒左邻居

7.放下右边叉子  放下右叉子,可能要唤醒右邻居

8.新循环开始,转1

哲学家的状态:思考,饥饿,就餐;

state来记录每个哲学家的状态

mutex互斥量,用来实现对于state读取赋值等操作的互斥性

s[N]信号量,用来唤醒邻居,实现同步操作

 

对于每个哲学家,思考,拿起两把叉子或者被阻塞,吃饭,放下两把叉子

 

由于需要操作state共享资源,因此首先加互斥锁,当前状态设为饥饿状态,试图拿起两把叉子,如果拿到两把叉子,则开始吃饭,如果拿不到便阻塞。

当前状态为饥饿状态,左右邻居非吃饭状态,则表示可以拿起两把叉子,当前状态设为吃饭状态,通知当前哲学家可以吃饭

放叉子,需要操作共享资源state,因此需要加锁,然后修改状态为思考状态,看左邻居和右邻居是否可以进餐。


5.读者-写者问题

读者:只读取数据,不修改;写者:读取和修改数据

同一时刻,允许有多个读者同时读;没有写者时读者才能读,没有读者时写者才能写;没有其他写者时写者才能写;

5.1读者优先----用信号量解决读者-写者问题

信号量WriteMutex:控制读写操作的互斥,初始化为1

读者计数Rcount正在进行读操作的读者数目,初始化0

信号量CountMutex:控制对读者计数的互斥修改,初始化1

首先用WriteMutex来保证读者和写者的互斥性

 

如果当前只有一个读者,需要获取WriteMutex来保证读者和写者互斥性;如果有多个读者,则可以直接rount++,进行读操作;

读操作结束之后,rount--,如果当前已经没有读者,则需要唤醒可能存在的写者。

  

由于可能存在多个读者,即对于rount的操作必须保证互斥性,即通过CountMutex来保证互斥性

只要有读者正在读状态,后来的读者都能直接进入,如读者持续不断进入,则写者就处于饥饿

5.2写者优先----用管程解决读者-写者问题

只要有写者就绪,写者应尽快执行写操作。

读者实现

记录活跃等待读者 活跃等待写者数量,互斥锁,两个条件变量用来同步。

对于读者,如果当前没有写者,则开始执行读操作;如果读完之后,则应该唤醒可能存在的写者。

由于需要对WR,AR共享变量操作,因此需要首先获取锁;如果当前有写者AW+WW>0,则该读者应该等待,WR++,等待;

结束读操作,AR--,如果当前没有活跃读者并且有等待写者,则通知等待写者可以写

写者实现

如果当前有活跃写者或读者,则写者应该等待

写结束之后,如果还有等待写者,则唤醒等待写者;如果还有等待读者,则唤醒所有等待读者进行读操作。

参考:清华大学 操作系统  陈渝  http://os.cs.tsinghua.edu.cn/oscourse/OS2015/


最新回复(0)