进程间通信(Inter Process Communication, IPC)要解决三个问题:
(1)进程间如何传递信息
(2)确保两个或更多进程在关键活动中不会出现交叉
(3)有协作关系的进程的时序问题
竞争条件(race condition)
定义:多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序。
互斥(mutual exclusion)
定义:对于个共享数据,同一时刻只有一个进程操作他。
临界区(critical region)
定义:对共享内存进行访问的程序片段称作临界区
避免竞争条件的一个好的解决方案还应该保证并发进程能够正确和高效地进行协作,因此要满足一下条件:
(1)任何两个进程不能同时处于其临界区
(2)临界区外运行的进程不得阻塞其他进程
(3)不得使进程无限期等待进入临界区
(4)不应对CPU的速度和数量做任何假设
互斥方案
当一个进程进入临界区其他个进程将不可以进入,这样就不会带来麻烦;//ps同步代码块?
(1)屏蔽代码块
CPU在发生时钟中断时才会切换线程,进程进入临界区后屏蔽掉所有中断,并在离开临界区时打开中断,这样就可以实现同一时刻只有一个进程读写共享数据。
缺点:(1)屏蔽中断地权力交给用户时不明智地(2)屏蔽中断仅对执行disable指令的CPU有效;
屏蔽终端对操作系统本身而言是很有用的(屏蔽中断以刷新数据),但对用户进程而且是一种不合适额互斥机制。
(2)锁变量
共享(锁)变量 锁为0 表示没有进程进入临界区,否则有进程进入临界区
锁自身也是共享数据,也存在竞争,锁是不安全的!
(3)严格轮换法(自旋锁)
忙等待:while(true){...};
严格轮转法采用忙等待,即连续测试一个变量直到某个值出现为止,用于忙等待的锁称为自旋锁(spin lock),这种方式比较浪
费CPU时间,通常应该避免。此方法会阻塞其他进程所以不是一个好的方法。
(4)peterson解法
int turn; //锁变量
int interested[N];
void enter_region(int process){
int other;
other = 1 - process; //其他进程
interested[process] = True;
turn = process;
while(turn==process && interested[other]==True); //等待other离开临界区
}
void leave_region(int process){
interested[process] = False;}
(5)TSL(Test and Set Lock)
需要硬件支持的一种方案:将一个内存字lock独到寄存器中,然后在该内存地址上存一个非0值,读字和写字操作保证是不可分割的,即指令结束之前其他处理器均不允许访问该内存字。(是不是类似CAS【compare and swap】?),执行TSL指令的CPU将锁住内存总线,以禁止其他CPU在本指令结束之前访问内存。
(6)睡眠与唤醒( sleep and wakeup【生产者和消费者问题/有界缓冲区】)
睡眠是将一个无法进入临界区的进程阻塞,而不是忙等待,该进程被挂起,直到另外一个进程将其唤醒。
#define N 100 //缓冲区大小
int count = 0;
//数据生产者
void producer(void){
int item;
while(True){
item = produce_item(); //产生下一新数据项
if(count==N) sleep(); //如果缓冲区满,就进入休眠状态
insert_item(item); //将新数据项放入缓冲区
count++;
if(count==1) wakeup(consumer);//唤醒消费者
}
}
//数据消费者
void consumer(void){
int item;
while(True){
if(count==0)sleep(); //缓冲区为空,就进入休眠状态
item = remove_item(); //从缓冲区取走一个数据项
count--;
if(count==N-1)wakeup(producer);//唤醒生产者
consume_item(item);
}
}
在生产者-消费者(producer-consumer)问题中,两个进程共享一个公共的固定大小的缓冲区(bounded-buffer),其中一个是生产者,将信息放入缓冲区;另一个是消费者,从缓冲区取走信息。当缓冲区满时,让生产者睡眠,待消费者从缓冲区取出一个或多个数据项时再唤醒它。同样地,当消费者试图从空缓冲区取数据时,消费者就睡眠,直到生产者向其中放入一些数据项时再唤醒它。
存在的问题:缓冲区的计数器count 并不是安全的,该共享资源会出先竞争条件。当判断count满足睡眠条件时,但还未sleep,此时收到的wakeup信号将不起作用,倒是wakeup信号丢失;
解决方法:启用唤醒等待为,当放松一个wakeup信号之后将wakeup置1,在睡眠之前判断安wakeup位是否为1 ,如果是则不sleep 并清楚wakeup位。
信号量Semophore
原子操作:一组相关联的操作要么都不间断的执行,要么都不执行。
信号量是设置一个整型变量来累计唤醒次数。对信号量有两种操作:down和up(一般化后的sleep和wankeup)。
down操作,则是先检查其值是否大于0,若该值大于0,则将其值减1并继续,若该值为0,则进程将睡眠。这里,检查数值、修
改变量值以及可能发生的睡眠操作是一个原子操作。
up操作对信号量加1,信号量的增值1和唤醒操作同样是不可分割的。
二元信号量:保证同时只有一个信号量进入临界区 其值为0 、1;
信号量优化生产者消费者问题
#define N 100 //缓冲区大小
typedef int semaphore;
semaphore full = 0; //缓冲区已用数目
semaphore empty = N; //缓冲区剩余数目
semaphore mutex = 1; //控制对临界区的访问
//数据生产者
void producer(void){
int item;
while(True){
item = produce_item(); //产生下一新数据项
down(&empty);
down(&mutex); //进入临界区
insert_item(item);
up(&mutex); //离开临界区
up(&full);
}
}
//数据消费者
void consumer(void){
int item;
while(True){
down(&full);
down(&mutex);
item = remove_item();
up(&mutex);
up(&empty);
consume_item(item);
}
}
互斥量
没有信号量的计数能力,信号量的一个简化版本,互斥量是一个可以处于两态之一的变量:加锁和解锁
如果互斥量是解锁的,则线程可以自由进入临界区,如果互斥量是加锁的,调用线程被阻塞,直到临界区中的线程完成并调用mutex_unlock,如果多个线程被阻塞在互斥量上将随机选择一个线程允许它获得锁。
互斥锁的实现依靠TSL或者XCHG指令(JAVA CAS?)
条件变量
在C语言中
{
pthread_mutex_lock(&mutex);
while(...) pthread_con_wait(&condp,&mutex);
......
pthread_mutex_unlock(&mutex);
}
{
pthread_mutex_lock(&mutex);
pthread_con_siganl(&condc);
......
pthread_mutex_unlock(&mutex);
}
条件变量允许线程由于一些未达到的条件而阻塞;
条件变量和互斥量经常一起使用,用于让一个线程锁住一个互斥量,然后当他不能获得他期待的结果是等待一个条件变量,最后另一个线程会向他发送信号,然后继续判断条件;
条件变量不是计数器,条件变量也不能像信号量那样累加以便以后使用,所以如果向一个条件变量发送信号,但此时条件变量并没有在条件上阻塞(等待进程),则该信号无效(丢失)。换而言之,wait必须在signal之前。
管程(monitor)
一个管程是一个由过程、变量及数据结构等组成的一个集合,它们组成一个特殊的模块和软件包。进程可在仍需需要的时候调用管程的过程,但不能在管程之外申明的过程中访问管程的数据结构。
管程中任一时刻管程中只能有一个进程活跃。java可以实现管程
通过将信号量放在共享内存中并使用TSL或者XCHG指令来保护他们可以壁面竞争,但是在一个分布式系统中(局域网相连)这些原语将失效。
消息传递
send
receive
如果没有消息可用,接收者可能被阻塞,直到一条消息到达,或者带着一个错误码立即返回。
屏障
用于进程组之间同步的。
将应用划分为若干阶段,只有当所有线程都准备就绪着手下个阶段,否则任何进程都不能进入下个阶段,通过在阶段结尾设置屏障来拦截先到的线程。
java中的java.util.concurrent.CyclicBarrier (栅栏) 就是屏障的一个实现