系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区取出一个产品并使用;缓冲区在同一时刻只能允许一个进程访问。
typedef struct{ int value; Struct process *L; //等待序列 }semaphore; semaphore full; semaphore empty; semaphore mutex; //某进程需要使用资源时,通过wait原语申请:P操作 void wait(semaphore S){ S.value--; if(S.value < 0){ block(S.L);//阻塞原语,将当前进程挂载到当前semaphore的阻塞队列 } } //进程使用完资源后,通过signal原语释放:V操作 void signal(semaphore S){ S.value++; if(S.value <= 0){ wakeup(S.L);//唤醒原语,将当前semaphore的阻塞队列中的第一个进程唤醒 } } full.value=0; //缓冲区 “产品”资源数(初始为0),用于实现生产者与消费者进程的同步 empty.value=n; //缓冲区 “空位”资源数(初始为n),用于实现生产者与消费者进程的同步 mutex.value=1; //互斥信号量,用于实现所有进程之间互斥地访问缓冲区 //生产者 producer(){ while(1){ Produce(); //生产“产品” P(empty); //“空位”数-1 P(mutex); //临界区上锁 Storage(); //摆放产品 V(mutex); //临界区解锁 V(full); //“产品”数+1 } } //消费者 consumer(){ while(1){ P(full); //“产品”数-1 P(mutex); //临界区上锁 TakeOut(); //拿走产品 V(mutex); //临界区解锁 V(empty); //“空位”数+1 Use(); //使用“产品” } }
对于各个操作顺序的理解:
对于一部分操作的顺序,我们很好理解,符合我们的认知:
//生产者 Produce(); //生产产品 P(empty); //“空位”数-1,可能有人在这里会问这个操作为什么不可以放在“Storage甚至是V(full)”后面,考虑一种情况:当空位数为0时,我们是不能摆放产品的,而这个操作正是在检查是否还有“空位”这种资源;所以它一定在Storage前面。 Storage(); //摆放产品 V(full); //“产品”数+1,可能有人在这里会问这个操作为什么不可以放在“Storage甚至是P(empty)”前面,考虑一种情况:当产品数为n时,我们是不能再摆放产品的,因为缓冲区已满,再向其中添加数据(执行Storage)是要出问题的;所以它一定在Storage后面。
//消费者 P(full); //“产品”数-1,同上面一样,可能有人在这里会问这个操作为什么不可以放在“TakeOut甚至是V(empty)”后面,考虑一种情况:当产品数为0时,我们是不能拿走产品的,而这个操作正是在检查是否还有“产品”这种资源;所以它一定在TakeOut前面。 TakeOut(); //拿走产品 V(empty); //“空位”数+1,同上面一样,可能有人在这里会问这个操作为什么不可以放在“TakeOut甚至是P(full)”前面,考虑一种情况:当空位数为n时,我们是不能拿走产品的,因为缓冲区已经空了,再拿走(执行TakeOut)拿走个寂寞;所以它一定在TakeOut前面。 Use(); //使用“产品”
对于其他操作:实现同步的P操作一定要在实现互斥的P操作之前,为什么呢?
反向分析:我们考虑若调换生产者上述两个P操作的顺序:
//生产者 producer(){ while(1){ Produce(); //生产产品 P(mutex); //临界区上锁 P(empty); //“空位”数-1 Storage(); //摆放产品 V(mutex); //临界区解锁 V(full); //“产品”数+1 } } //消费者 consumer(){ while(1){ P(full); //“产品”数-1 P(mutex); //临界区上锁 TakeOut(); //拿走产品 V(mutex); //临界区解锁 V(empty); //“空位”数+1 Use(); //使用“产品” } }
说白了,对于进程来说,阻塞前对临界资源上锁,就是占着茅坑不拉屎(粗俗的讲:死锁就是两个人都占着茅坑不拉屎,还都等着对方离开然后在别人茅坑里才能拉)。
Produce和Use可以放在临界区吗?
V操作不会使进程阻塞,故互斥和同步的V操作顺序从理论上来说可以调换,但同3一样,为了进程快速交替使用临界资源,要让临界区代码尽量短,所以不把同步的V操作放在临界区代码中
综上:不要往临界区中添加与访问临界资源无关的操作/代码