进程与线程:同步&互斥
同步&互斥的概念
进程具有异步性的特征。异步性是指各并发进程执行的进程的以各自独立的,不可预知的速度向前推进
同步
同步 亦称为直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程的直接制约关系就是源于它们之间的相互合作。
互斥
互斥 也称为间接制约关系,是指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。当前访问临界资源的进程访问结束,释放该资源后另一个进程才能去访问临界资源。
临界资源
将一次仅允许一个进程使用的资源称为临界资源。对临界资源的访问,必须互斥地进行,可把临界资源的访问过程分为4个部分:
- 进入区。 在进入临界区使用临界资源前,检查是否可进入临界区,若能进入则设置正在访问的临界区的标志,以防止其他进程同时进入临界区。
- 临界区。 进程中访问临界资源的代码
- 退出区。 将正在访问临界资源的清除(类似于解锁)
- 剩余区。 代码中的其余部分
注:进入区和退出区是负责实现互斥的代码段;临界区是进程访问临界资源的代码段
进程互斥需要遵守以下原则:
- 空闲让进。 临界区空闲时,可允许一个请求进入临界区的进程立即进入临界区
- 忙则等待。 当已有进程进入临界区时,其他试图进入临界区的进程必须等待
- 有限等待。 对请求访问的进程,应保证能在有限时间内进入临界区
- 让权等待。 当进程不能进入临界区时,应立即释放处理机,防止进程忙等待
进程互斥的基本实现方法
软件实现方法
单标志法
单标志法算法思想:两个进程在访问临界区后会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋予。
代码的逻辑其实就是,刚开始通过turn变量控制只能让一个进程进入临界资源,当该进程访问结束后再修改trun的值让别的进程可以访问。
缺陷:使用单标志法,只能让进程进行交替地轮流访问,如果此时允许进入的临界区的进程是P0,但是P)一直不访问,那么虽然临界区空闲,但是并不允许,P1访问。违反了空闲让进的原则。
双标志先检查
双标志先检查算法思想:使用一个bool数组flag[],数组中各元素用来标记各进程想进入临界区的意愿。所以当进程要进入临界区前会去检查别的进程在flag中表达的是否访问临界资源。
这个算法的过程大致如下:每次在进入区通过flag数组来检查别的进程是否访问临界资源,若无访问则通过flag[i] = true来表达自己开始访问了。
缺陷:当两个进程同时进入进入区时,将会导致两个进程同时进入临界区而导致错误。可以设想P0进程刚通过上图中的第①条语句后,发生进程的切换,P1进程开始运行,此时flag[0]还未修改,所以P1进程也能进入临界区。所以,双标志的主要问题是违反了忙则等待的原则。原因在于,进入区的检查和上锁两个处理并不是一气呵成的。
双标志后检查
双标志后检查算法思想:双标志先检查的改版,其实就是先“上锁”后“检查”的方法。
这个算法的缺陷和双标志先检查的差不多,都是当一个进程执行了第①条语句后发生切换时,将会导致两个进程同时标志flag[i]=true,将导致两个进程都无法进入临界区。所以双标志后检查法违反了“空闲让进”和“有限等待”原则,各进程会长期无法访问临界资源而产生饥饿
现象
Peterson算法
Peterson 算法思想:结合双标志法、单标志法的思想。
这个算法符合空闲让进、忙则等待、有限等待三个原则的核心是,在用while语句检查进入临界区前,使用了trun值的唯一性保证了进程之间的互斥性。但是未遵守让权等待原则,会发生忙等。
硬件实现方法
关中断屏蔽
利用开/关中断指令
实现(与原语的实现思想相同,即进程开始访问临界资源到结束访问的行为都不允许被中断,也就不能发生进程的切换,因此不会发生两个同事访问临界区的情况)
缺点:不适合多处理机;只适用于操作系统内核进程,不试用于用户进程(因为开中断和关中断指令只能运行在内核态)
TestAndSet
简称TS指令,也可称为TestAndSetLock,或TSL指令。
TSL指令由硬件实现,执行过程不允许被中断,只能一气呵成。下图为用c语言描述的逻辑:
TSL其实借助了硬件的实现,与软件的实现方法相比,将上锁
和检查
两个操作变成原子操作。
优点:实现简单;适合多处理机环境
缺点:不满足“让权等待”原则,暂时无法进入临界区的进程会占用CPU并循环执行TSL指令,从而导致忙等
Swap指令
也叫Exchange指令,或简称XCHG指令。
Swap指令通过硬件实现,执行过程不允许被中断,只能一气呵成,以下是用C描述实现的逻辑
逻辑上来看Swap和TSL并无太大的区别,都是先记录下此时临界区是否已经被上锁,再将上锁标志设置lock设置为true,最后检查old,若old为false则说明之间没有别的进程对临界区上锁,可进入临界区
缺点:不满足让权等待原则,暂时无法进入临界区的进程会占用CPU并循环执行Swap指令,从而导致忙等
锁
解决临界区最简的工具就是互斥锁(mutex lock)。一个进程在进入临界区时获得锁;在退出临界区时释放锁。函数acquire()
获得锁,函数release()
,这两个函数的执行都是原子操作,因此互斥锁通常采用硬件机制实现
互斥锁通过设置一个bool变量available,表示锁的是否可用。若锁是可用的,调用acquire()会成功,当锁不再可用,试图获取不可用锁时,会被阻塞,知道锁被释放。
互斥锁的主要缺点是忙等待,互斥锁通常用于多处理器系统,一个线程可以在一个处理器上等待,不影响其他线程的执行。
需要连续忙等的互斥锁,都可称为 自旋锁(spin lock),如TSL指令、Swap指令、单标志法。
特征:
- 需要忙等,进程时间片用完才下处理机,违反“让权等待”
- 优点:等到期间不用切换进程上下文,多处理器系统中,若上锁时间段,则等待代价低(因为当进程在正在阻塞的这个时间片内,有可能被解锁)
- 常用于多处理器系统,一个核忙等,其他核照常工作,并快速释放临界区
- 不太实用单处理机系统,忙等的过程中不可能解锁
✨✨✨信号量机制
用户进程可以通过操作系统提供的一对原语来对信号量进行操作,从而实现进程的互斥、同步
信号量其实就是一个变量(可以是整数,也可以是更复杂的记录型变量),可以用于一个信号量来表示系统中某种资源的数量,比如,系统中有一个打印机,就可以设置一个初值为1的信号量
原语是一种特殊的程序段,其执行只能一气呵成,不可被中断。原语的实现是由关中断/开中断指令实现
一对原语:wait(S)(申请资源)原语和 signal(S)(释放资源)原语,其中的S,其实就是信号量
wait、signal原语常简称为P、V操作(对应荷兰语的proberen和verhogen)。因此,做题时常把wait(S)、signal(S)两个操作分别写为P(S)、V(s)
整型信号量
用一个整数类型的变量作为信号量,用来表示系统中某种资源的数量。依然不满足让权等待的原则。
✨👀记录型信号量
==记录型信号量机制是一种不存在“忙等”现象的进程同步机制。==记录型信号量,相对于之前的做法多了一个等待队列,当进程需要的信号量不够时,则将该进程通过等待队列记录下来,并且将该进程从运行态改为阻塞态
注意记录型信号量机制中,信号量的变化位置,可以关注以下几点:
- 当信号量s为负数, ∣ s ∣ |s| ∣s∣表示了正在等待的进程数
- 每当有进程进程调用signal释放资源时,就从等待队列中调出一个等待的进程,并给信号量加一表示等待的进程数量减少1
- 信号量初始值s,当每个进程运行时,只需要1个信号量时,s即表示该资源,最多被多少个进程共享
信号量机制实现进互斥
实现进程的互斥:
- 分析并发进程的关键活动,划定临界区(如:对临界资源打印机的访问应该放在临界区)
- 设置互斥信号量mutex,初值为1
- 在进入区P(mutex)——申请资源
- 在退出区V(mutex)——释放资源
注意:对不同的临界资源需要设置不同的互斥信号量。P、V操作必须成对出现
信号量机制实现进程同步
用信号量实现进程同步:
- 分析在什么地方需要实现“同步关系”,必须保证“一前一后”执行的两个操作
- 设置同步信号量S,初始值为0
- 在“前操作”之后执行V(S)
- 在“后操作”之前执行P(S)
可以记为前VP后
。
理解:要实现进程的同步,就是让上进程做到按一定先后次序执行。利用刚开始设置信号量为0,让直接执行P操作的进程进入阻塞态(因为此时信号量为0,表示无任何资源),而先执行的进程则直接执行,在执行完后执行V操作,类似地给系统增加这个信号量,从而唤醒被阻塞的进程,所以可以发现当两个同步的进程执行完后信号量S变为了1
信号量机制实现前驱关系
进程P1中有句代码S1,P2中有句代码S2,P3中有句代码S3……S6中有代码S6,要让这些代码按照一定的顺序关系来执行。对于这种前驱关系都是一个进程同步问题(需要保证一前一后的操作),所以其实就是多次进程的同步,所以只要:
- 为每一对前驱关系各设置一个同步信号
- 在“前操作”之后相应的信号量(注意区分不同的信号量)执行V操作
- 在“后操作”之前相应的信号量执行P操作
经典同步问题
生产者消费者问题
改变相邻P、V操作的顺序,将会出现死锁
若将前面的生产消费的代码进行更改,如上图。
若此时生产者执行①使mutex变为0,再执行②,由于没有空闲缓冲区,因此生产者被阻塞。由于生产者被阻塞,因此换回消费者进程。消费者进程执行③,由于mutex为0,即生产者还没有释放对临界区资源的“锁”,因此消费者也被阻塞。
这就造成了生产者等待消费者释放空闲缓冲区,而消费者又等待生产者释放临界区的情况,生产者和消费者循环等待被对方唤醒,出现“死锁”。
实现互斥P操作一定要在实现同步操作P操作之后(只调换一个也不行)。V操作不会导致进程阻塞,因此两个V操作顺序可以交换。
另外的思考:可以这么理解为什么上述互斥操作的P和同步操作的P不能互换位置,可以这么考虑,若实现互斥操作的P在前,实现同步操作的P在后,说明此时实现同步的操作的P也变为了互斥的关系,而实际上原来需要的同步操作的两个P是不互斥的,两者必定能实现一个。所以原来的循环才得以实现。而当互斥的P在前时,则打破了这种循环使得进入死锁。
多生产者—多消费者
不过这里,可以将上述图片中的对盘子资源的互斥给去除。
吸烟者问题
问题的关键点:
- 如何实现轮流smoke
- 如何表达消费者所需的资源
实现的关键:
- 使用变量i来实现生产者对不同产品的按顺生产
- 将消费者所需的资源抽象成一个整体
读者写者文件
思考:总体上看写者进程和若干个读者进程形成互斥关系,读者和读者之间也成互斥关系。所以对文件的写操作,实际上是一种互斥操作。所以可以整体来看这个互斥关系,逻辑上两个互斥实事件AB只需要
P(mutex)AV(mutex)
,P(mutex)BV(mutex)
.所以可以将读者写者问题转化为(P(mutex)写操作V(mutex)
,P(mutex)所有的读操作V(mutex)
)。然后只需表示出所有的读操作这个占用段,(P(mutex)写操作V(mutex)
,P(mutex)第一个读……最后一个读V(mutex)
)
解决读者进程“饿死”的实现:
哲学家进餐问题
这个问题的特点是,哲学家需要同时持有两个临界资源,才可以顺利执行,若临界资源总数少于消费者数时,需要可以将访问两种临界资源的看作一个整体的临界资源。这样可以保证起码有一个哲学家同时拿到一个临界资源(通过保证一次只有一个消费者征用),而当他完成任务后,就会释放自己的临界资源
管程
在信号量机制中,每个要访问临界资源的进程都必须自备同步操作,大量的同步操作会导致难以管理,于是就出现了管程。
管程是一种特殊的软件操作,它由以下几个部分组成:
- 局部于管程的共享数据结构说=说明
- 对该数据结构进行操作的一组过程
- 对局于管程的共享数据设置初始值的语句
- 管程的名字
管程的基本特征:
- 局部于管程的数据只能被局部于管程的过程所访问
- 一个进程只有通过调用管程内的进程才能进入管理程序访问共享数据
- 每次仅允许一个进程 在管程内执行某个内部过程
管程由编译器负责实现各进程互斥地进入管程的过程