多线程基础
进程间通信需要解决三个问题:
- 一个进程如何把消息传给另一个;
- 确保两个或更多的进程在关键活动中不会出现交叉,例如,飞机订票系统中两个不同的进程试图为两个客户争夺飞机上的最后一张票;
- 保证进程间的先后顺序,如进程A产生数据,进程B打印数据,则进程B在进程A完成之间必须等待。
第一个问题对线程而言比较容易,因为多个线程共享一个进程的地址空间(在多个地址空间通信的多个线程属于进程间通信的范畴)。其他两个问题对于线程而言同样适用。
概念1. 竞争条件
- 定义:两个或多个进程读写某些共享数据,而最后结果取决于进程运行的精确时序,成为竞争条件(race condition)。
- 实例:当一个进程需要打印一个文件,它将文件名放在一个特殊的假脱机目录下。另一个进程(打印机守护进程)则周期性地检查是否有文件需要打印,若有就打印并把该文件名从目录下删掉。几乎在同一时刻,进程A和进程B都决定将一个文件排队打印,这时可能进程A读到目录中下一个空闲槽位(用变量in表示)是7,将7存到自己的局部变量next_free_slot中,这个时候来了一个时钟中断,CPU认为进程A已运行了足够长的时间。决定切换到进程B。进程B读到的in值也是7,于是将7存到B的局部变量next_free_slot中,这一时刻,两个进程都认为下一个可用槽位是7.这个时候,进程B继续运行,将要打印的文件名写入到槽位7中,将下一个空闲槽位的值in修改为8.这个时候进程A运行读到的in值是7,将要打印的文件名写入到槽位7中,这样就覆盖了进程B存在槽位7的待打印文件(再也不会执行了)。这时进程A也将in值修改为8.这时家若即内部目录是一致的,所以守护进程发现不了任何错误,但是进程B永远得不到任何输出。
概念2. 临界区
要解决上述错误,关键是要阻止多个进程同时读写共享的数据。换言之,就是要实现互斥(mutual exclusion)。一个进程在一部分时间可能做一些内部运算或是其他不会引起竞争条件的部分,而某些时间则需要对共享内存或是共享文件进行访问,或执行另外一些导致竞争的操作。
- 定义:我们把对共享内存进行访问的程序片段称为临界区域(critical region)或临界区(critical section)。通过适当的安排,使得两个不同的进程不可能同时处于临界区中,就能有效地避免竞争条件。
概念3. 忙等待的互斥
如何利用临界区避免竞争条件?:①屏蔽中断,每个进程进入临界区后,屏蔽所有中断(进行进程切换的必要条件),再要离开之前打开中断。但这个一方面存在风险,因为将屏蔽中断的能力交给用户进程,有可能因为用户进程不再打开中断导致系统终止,另一方面在多处理器上不适用,因为屏蔽中断只对其所运行的CPU有效,这样仍有可能发生竞争条件。所以这个依赖于CPU的数量。②锁变量,某个进程检测到锁变量是0,就将其修改为1,进入到临界区,如果是1,就等待。这个会出现类似假脱机目录一样的问题。③严格轮转法,每个进程进入临界区的条件对应于轮转变量的一个特定值,例如轮转变量为1,A进程进入临界区,为2,B进程进入临界区。但是这有可能进程被临界区之外的进程阻塞的情况:轮转变量为1=>A进程进入临界区=>A出临界区,将轮转变量修改为2=>A进入非临界区,B进入临界区=>B出临界区,将轮转变量修改为1,A运行完非临界区代码,B开始运行非临界区代码=>A重新进入临界区,出临界区,修改轮转变量为2. 这个时候加入A进程很快运行完非临界区代码,而B进程还在运行非临界区代码(可能还需要更长时间),这时A就会被临界区之外的进程阻塞。