什么是同步?
简而言之,同步指的是如何为进程按照一定的次序合理地分配资源,防止对共享变量或共享资源造成不正确的访问,从而保证结果的正确性。(即当多个进程申请访问同一资源时,某些进程可能需要等待,不能同时访问资源)
与同步问题紧密相关的是临界区问题。首先我们来了解一下什么是临界区。
什么是临界区?
临界区是一段代码,进程在执行该段代码时可能修改公共变量。
- 当一个进程在临界区内执行时,其他进程不允许在他们的临界区执行(即临界区内在一个时刻只能有一个进程)。
-
在进入临界区前,每个进程应当在进入区请求;当进程要退出临界区时,进程会进入到退出区。
现在我们了解了临界区的基本概念,那么我们应该如何来解决临界区问题以实现同步呢?
临界区问题的解决方案应满足三个条件:
1)互斥:只能有一个进程在临界区内执行
2)进步:当有进程请求进入临界区时,应当在请求区中选择一个进程进入临界区,这种选择不能无限推迟
3)有限等待:保证请求进入临界区的进程在经历一段时间的等待后,会进入到临界区内
现在我们来简要介绍两种解决临界区问题的具体方法:1.互斥锁
设置一个全局布尔变量(所有进程共享),表示锁是否可用(即是否可进入临界区)。首先进程执行acquire函数,如果可进入临界区,那么修改该布尔变量(即修改为不可进入临界区),然后进入临界区执行,当执行结束退出临界区时,执行release函数,把该布尔变量修改回来(修改为可进入临界区);如果不可进入临界区,那么进程执行循环代码,直到该布尔变量表示可进入临界区。
- 互斥锁必须保证acquire函数和release函数的调用都是原子的。
缺点
需要忙等待(即进程要不断地执行循环代码),消耗大量的cpu时间。
优点进程在等待锁时,没有上下文切换。
2.信号量
用整型变量S表示剩余可进入临界区的进程数量(S的值根据S所代表资源的数量而定,一般设置为1);拥有两个原子操作wait()和signal(),即P和V操作。以S=1为例:进程首先执行wait()函数,先令S--;若S>=0,那么让该进程进入临界区;否则将该进程放入一个等待队列中等待被唤醒。当进程从临界区退出来时,执行signal()函数,令S++,如果S<=0,那么从等待队列中选择一个进程来唤醒(将其放到就绪队列的最前端);否则此时S=1,说明已经没有进程申请进入临界区,等待队列为空。
缺点上下文切换频繁。
优点
不需要忙等待;如果无法进入临界区,将进程放进等待队列即可,cpu可以去服务其他进程。
互斥锁和信号量两种解决临界区问题的方法已经简要介绍完了,下面我们来看几个经典的同步问题。大家可以考虑一下要如何利用信号量等方法去解决实际的同步问题。
经典同步问题
生产者-消费者问题(有限缓冲问题)
仓库就是一个临界区,生产者和消费者每次只能有一个进入临界区。仓库为空时,消费者必须等待;仓库满时,生产者必须等待。
- 可以用信号量代表仓库的容量,利用互斥锁来解决同一时刻只能由一个生产者或消费者进入临界区这一问题;进入临界区之前首先判断进程是消费者还是生产者,当仓库为空时,消费者执行wait();当仓库为满时,生产者执行wait()。
读者-写者问题
- 1)允许多个读者同时读
- 2)读的时候不能写,写的时候不能读
- 3)多个写者不能同时写
第一读者-写者问题要求读者优先(即既有读者在排队,也有写者在排队,那么优先让排队的读者进入临界区);这可能导致写者饥饿。
第二读者-写者问题要求写者优先(当既有读者,也有写者在排队时,那么优先让写者进入临界区);这可能导致读者饥饿。
哲学家吃饭问题
一个圆桌上,总共五个哲学家,五根筷子。当一个哲学家同时拥有左右两双筷子时,该哲学家才可以吃饭。当每一位哲学家都拿起同一侧的拿根筷子时,那么会出现死锁的现象,导致所有哲学家都占有且等待,无法进行吃饭。 解决哲学家吃饭问题的死锁问题,有三种方法:
1)最多允许四位哲学家同时坐在桌上(也就是说有一位哲学家不能拿起筷子),这样可以保证一个哲学家能够吃到饭,解决了循环等待的问题,不会出现死锁现象。 2)当一个哲学家可同时拿起左右两根筷子时,再拿起两根筷子进行吃饭;否则不拿筷子。这样解决了占有且等待(等待的不会占有)的问题,不会出现死锁现象。
3)采用非对称的拿筷子方式。单号哲学家首先拿起左边的筷子,再拿右边的筷子;双号哲学家首先拿起右边的筷子,再拿左边的筷子。这样可以保证相邻的两个哲学家必有一个可以吃到饭,解决了循环等待的问题,不会出现死锁现象。
全部评论
(2) 回帖