引言
上文我们了解了一些关于信号量机制的一些经典应用,知识点比较繁杂,可能会有点啰嗦,但是对初学者而言还是越详细越好。接下来我们介绍一下管程。
管程
管程是程序设计语言结构,可提供易于控制的同步机制,我们熟知的Java中就有相关的实现。
- 互斥
各外部进程/线程只能通过管程提供的特定“入口”才能访问共享数据。
一个管程的程序在执行一个线程前会先获取互斥锁,直到完成线程或是线程等待某个条件被满足才会放弃互斥锁。若每个执行中的线程在放弃互斥锁之前都能保证不变量成立,则所有线程皆不会导致竞态条件成立。
以下这个银行账户的提款/存款事务的管程是个简单的例子:
monitor class _Account_ {
private _int_ balance := 0
invariant balance >= 0
public method _boolean_ withdraw(_int_ amount)
precondition amount >= 0
{
if balance < amount then return false
else { balance := balance - amount ; return true }
}
public method deposit(_int_ amount)
precondition amount >= 0
{
balance := balance + amount
}
}
在上述银行账户的管程中,互斥锁用于确保在进行提款或存款操作时不会发生数据竞态条件。每个线程在执行提款或存款操作时,都需要先获取互斥锁,然后执行相应的操作,最后释放互斥锁。
假设每个执行中的线程在放弃互斥锁之前都能保证不变量成立,即在释放互斥锁之前都能确保账户余额的不变量(balance >= 0)成立。这意味着无论是进行提款还是存款操作,线程在释放互斥锁之前都会确保账户余额不会出现负值。
考虑两种情况:
- 提款操作:线程在释放互斥锁之前会检查账户余额是否足够提取提款金额。如果账户余额不足,则不会执行提款操作,直接返回 false。这意味着即使多个线程同时尝试提款,只有一个线程会成功提款,因为其他线程会在检查余额时发现不足,从而放弃提款操作。因此,不会出现竞态条件。
- 存款操作:线程在释放互斥锁之前会直接将存款金额加到账户余额上,而不会有其他操作。因为存款金额必须大于等于0,所以存款操作不会导致账户余额变为负值。多个线程同时执行存款操作时,由于每个线程都会在加金额操作之前确保不变量成立,所以不会出现竞态条件。
综上所述,若每个执行中的线程在放弃互斥锁之前都能保证不变量成立,即确保操作前的条件正确,那么所有线程皆不会导致竞态条件成立。因此,银行账户的提款/存款事务的管程能够有效地避免竞态条件。
当一个线程执行管程中的一个子程序时,称为占用(occupy)该管程. 管程的实现确保了在一个时间点,最多只有一个线程占用了该管程。这是管程的互斥锁访问性质,即任何时候只有一个进程在管程中执行,这种特性由编译器实现,编程语言负责封装。
- 同步
通过条件变量condition
来获得同步,可以理解为一个同步信号量
-
cwait:直接在条件变量c上阻塞调用进程
-
csignal:直接释放条件变量c上某个阻塞进程;若无,返回
在这张图中展示的是一个监控(Monitor)结构的示意图。在并发编程中,监控是一种同步机制,用于控制对共享资源的访问,确保在任何给定时刻只有一个线程可以执行临界区代码。
图中详细展示了以下几个关键组件:
-
入口队列(Queue of entering processes):
- 这是一个等待区域,所有试图进入监控的进程都会首先进入这个队列。只有前面的进程退出监控后,队列中的下一个进程才可以进入。
-
本地数据(Local data):
- 监控内部的数据,被所有进入监控的过程共享。
-
条件变量(Condition variables):
- 用于控制进程在特定条件不满足时的等待和唤醒。每个条件变量可以有一个关联的等待队列,当条件未满足时,进程可以在此等待。
-
过程(Procedure 1 to Procedure k):
- 监控中定义的方法或过程,这些方法通过互斥保证在任何时刻只有一个进程可以执行。
-
紧急队列(Urgent queue):
- 在某些实现中,当一个进程在条件变量上被唤醒时(通过
csignal
),它可能会被放入一个特别的紧急队列。这意味着这个进程将在释放监控的互斥锁之前得到处理的优先权。
- 在某些实现中,当一个进程在条件变量上被唤醒时(通过
-
cwait 和 csignal 操作:
cwait(cn)
: 表示在条件变量cn
上等待。调用此操作的进程将阻塞,直到其他进程通过csignal
唤醒它。csignal(cn)
: 用于唤醒在条件变量cn
上等待的一个进程。如果没有进程在等待,则这个调用无效果。
图中还提到了两种主要的监控实现方式:Hoare 方法和Lampson/Redell 方法,这些不同的方法定义了条件变量的行为以及如何处理等待和唤醒的进程。
- Hoare 方法:在这种方法中,当一个进程被
csignal
唤醒时,发出信号的进程将直接将控制权交给被唤醒的进程。被唤醒的进程在退出监控前不会释放互斥锁。 - Lampson/Redell 方法:在此方法中,发出信号的进程在完成自己的剩余代码后继续执行,而被唤醒的进程将等待直到监控的互斥锁可用。
这样的结构和操作保证了对共享资源的安全访问和协调,并减少了死锁和竞态条件的可能性。
最后,本节内容总结如下:
这一小节的内容主要讲的是管程,管程是基于编程语言开发的一种程序结构,它为各外部进程/现成提供了特定的入口,且每次只允许一个进程在管程内执行某个内部过程(类似于函数调用)。管程通过编译器实现各进程之间的互斥,通过设置条件变量解决同步问题。