三个问题
在一个多进程程序中,一个进程通常要与其他进程交互,比如shell管道中,第一个进程输出通过管道送入第二个进程输入,下面讨论进程间通信(IPC)的问题。
第一个问题:进程如何把信息传给另一个。
第二个问题:如何确保两个或多个进程在关键活动中不会出现交叉,比如飞机订票系统中多个进程为不同客户试图抢夺最后一个座位。(互斥访问)
第三个问题:顺序问题,比如进程A产生数据,进程B打印数据,那么B在打印之前必须等A产生一些数据(条件同步)。
第一个问题对线程来说不是问题,因为线程共享一个地址空间,也就是说可以通过全局变量。
后两个问题对线程来说是同样使用的,同样的问题和解决方法也可应用于线程。
概念
竞争条件
多个进程并发读写某些共享数据,而最后的结果取决于进程运行的特定时序有关,称为竞争条件。
考虑两个进程对同一个共享变量count的修改:进程A执行++count,进程B执行–count,实际机器上++count可能的操作是:
reg1 = count
reg1 = reg1 + 1
count = reg1
同理–count:
reg2 = count
reg1 = reg2 - 1
count = reg2
并发执行++count和–count相当于按任意顺序执行上面的低级语句:
T0: reg1 = count
T1: reg1 = reg1 + 1
T2: reg2 = count
T3: reg1 = reg2 - 1
T4: count = reg2
T5: count = reg1
假设count初值是5,按上面顺序执行,count最终是6,如果交换T4,T5,count最终结果则为4。像这样就成为竞争条件。
临界区问题
多个进程共享的资源称为临界资源(比如共享内存共享文件打印机等),操作临界资源的代码段称为临界区。临界区问题是设计一个一边进程协作的协议,临界区问题的解答需满足三个条件:
- 互斥:一次只允许一个进程进入临界区
- 前进:如果没有进程在临界区执行,且有进程需要进入临界区,则选一个进入。
- 有限等待:不能使进程无限期进入临界区
临界区问题的解答
基于软件的解决:peterson算法
基于硬件的解决:禁止中断、原子操作指令等
以上是最基础的同步原语,使用不太方便,操作系统提供了更高级的编程抽象来简化使用:比如锁、信号量、条件变量等等。其实现用到了底层硬件的的解决。结构如下:
并发机制
采用《操作系统设计精髓与设计》一书中的定义:
在某些系统中,二元信号量与互斥锁没有区别,在linux中,二元信号量与互斥量的区别在于为互斥量加锁和解锁的进程必须为同一个进程,相比之下,可能某个进程为二元信号量加锁,而另一个进程为其解锁。
信号量
信号量是操作系统提供的一种机制,可以用来解决互斥访问和条件同步问题。信号量是一个整型变量,除了初始化外,只能通过两个原子操作P和V来访问。
P()操作使信号量sem减1,如果sem<0,进程则进入等待,否则继续。
V()操作时sem加1,如果sem<=0,则唤醒一个等待的进程。
信号量实现:
生产者消费者问题
描述如下:
使用信号量解决生产者消费者问题
问题分析:
- 任意时刻只有一个线程操作缓冲区(属于互斥访问)
- 缓冲区空时,消费者必须等待生产者(属于条件同步)
- 缓冲区满时,生产者必须等待消费者(属于条件同步)
用信号量描述约束
- 二进制信号量mutex
- 资源信号量fullBuffer
- 资源信号量emptyBuffer
问题解决:
管程
使用信号量可以解决互斥和同步问题,但是其使用比较复杂,不正确使用会导致一些时序错误,并且很难检测。因此要一种方法简化互斥和同步问题的解决。管程就是这么一种“方法”。管程是由一个或多个过程、一个初始化序列和局部数据组成的软件模块。具有以下特点:
- 局部数据变量只能有管程的过程访问,任何外部过程都不能访问。
- 一个进程通过调用管程的一个过程进入管程。
- 任何时候,只有一个进程能在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用。
管程结构如下:
管程内部使用条件变量来支持同步,(管程内部使用锁来控制过程的互斥访问,即内个过程开始时加锁,结束后解锁,当然,也可以视管程只有一个入口,一次只可一个线程进入,这其实是一样的),当一个线程在管程中时,可能会通过条件变量的wait操作阻塞在该条件变量上,并释放锁(即退出管程),以便于其他线程进入管程。
条件变量
条件变量时管程内的等待机制,进入管程的线程尹资源被占用而进入等待状态;每个条件变量表示一个等待原因,对应一个等待队列。条件变量有两个操作,wait和signal。
wait操作将调用线程阻塞,放入在条件变量的等待队列,并释放锁(即释放管程的互斥访问)。
signal操作将等待队列中的一个线程唤醒,如果队列中没有线程,就什么也不做。
实现如下:(只是原理性代码,都是原子操作)
使用管程解决生产者消费者问题
Deposit和Remove就是管程的两个过程,lock用来保证只有一个线程能进入管程调用过程。
Hansen管程和Hoare管程
两种管程的区别是对条件变量的释放处理方式不同。
Hansen管程因为线程T2 signal之后还在继续运行,有可能又将条件改写,导致虚假T1的虚假唤醒,因此具体使用时,都是用while来抱住wait,防止虚假唤醒。
虚假唤醒问题补充:
另一个可能的情况是虚假唤醒在linux的多处理器系统中/在程序接收到信号时可能回发生。在Windows系统和JAVA虚拟机上也存在。在系统设计时应该可以避免虚假唤醒,但是这会影响条件变量的执行效率,而既然通过while循环就能避免虚假唤醒造成的错误,因此程序的逻辑就变成了while循环的情况。