生产者和消费者问题是进程同步比较经典的问题之一.
问题是这样的:
一群生产者进程在生产产品,并将这些产品提供给消费者进程去消费,为使生产者进程和消费者进程能并发进行,在这两个之间设置了一个具有n个buffer的缓冲池,生产者进程将它所生产的产品放入一个缓冲区中,消费者进程可从一个缓冲区中取走产品去消费,虽然生产者和消费者都可以按照异步方式运行,但是它们之间必须保持同步,即不允许消费者进程到一个空缓冲区去取产品,也不允许生产者进程向一个已经满的缓冲区中添加产品.
我们来分析下这个过程:
使用一个拥有n个缓冲区的缓冲池,用输入in来指向可以添加产品的buffer, 每当生产者进程生产并添加一个产品后,输入指针加1,用一个输出指针out来指向可以获取的缓冲区,每当消费者进程取走一个产品后,输出指针+1 ,这里的buffer是个循环buffer. 所以输入指针加可以表示成 in = (in+1) mod n
当 (in + 1) mod n = out时表示缓冲区满,
而in = out 表示缓冲区空
这些其实也就是 环形队列 的重点了.
继续思考生产者和消费者问题...
每当生产者进程向缓冲区投放一个产品后,就让 counter这个变量 + 1 反之 ,每当消费者进程从中使用一个产品后, counter - 1 .指针 in 和 out 初始化为 1 . 让我们来用伪码表示下这个过程.
/* --- By Crazybaby --- */ Producer: while (1) { TempPro tPro = new TempPro; /**< 生产出一个产品 */ while (counter == n) { /**< 如果当前counter数等于buffer容量则执行空循环 */ ; } buffer[in] = tPro; /**< tPro临时存放生产出来的产品 */ in = (in + 1) % n; counter += 1; } Consumer: while (1) { while (counter == 0) { /**< buffer数目为空时,执行空语句 */ ; } tCon = buffer[out]; out = (out + 1) % n; counter -= 1; }一切看来都很好,但是这里隐晦着一个严重的bug,顺序执行是一点都没,若进行彼岸发执行就会出现错误,原因就在于 全局变量 counter .先执行生产者 执行完再执行 消费者 是没有问题的. 来看看这种情况:
设 counter 初始值为 3
produceCounter = counter ; /**< 3 */ produceCounter += 1; /**< 4 */ consumerCounter = counter; /**< 3 */ consumerCounter -= 1; /**< 2 */ counter = produceCounter; /**< 4 */ counter = consumerCounter; /**< 2 */ 正确的值应该为 3, 现在得出的值为 2,counter作为全局变量 ,这种情况就要考虑把counter作为临界区处理. 让生产者和消费者 互斥 的访问变量 counter.二元信号量 解决生产者和消费者问题
二元信号量是只有0 和 1的信号量. 这里 n 记录缓冲区中数据项的个数 , 信号量 s 用于 互斥, 信号量 delay 用于 缓冲区为空时 或者 满 时 等待.
int n ; Semaphore s = 1, delay 0; void producer() { while (1) { produce(); semWaitB(s); append(); n++; if (1==n) {semSignalB(delay);} semSignalB(s); } } void Consumer() { int m; semWaitB(delay); while (1) { semWaitB(s); take(); n--; m = n; semSignalB(s); consume(); if (0==m) {semWaitB(delay);} } }这里有两个原语要解释下:semWait 和 semSignal原语被假设是原子操作, 原型如下: struct Semaphore { enum {zero, one} value; queueType queue; }; void semWaitB (Semaphore s) { if (s.value == one) { s.value = zero; } else { /* --- 把当前的产品插入到缓冲区中 --- */ /* --- 注射当前进程 --- */ } } void semSignalB(Semaphore s) { is (s.queue is empty()) { s.value = one; } else { /* --- 把进程p从等待队列中移除 --- */ /* --- 把进程p插入到就绪队列 --- */ } }
所以信号量的互斥模型可以表示如下: while (1) { semWait(s); /* --- 临界区 --- */ semSignal(s); /* --- Others --- */ }
每个进程进入临界区前先执行semWait(s),如果s为负,则进程被挂起,如果值为1,则s-1 ,则进入临界区.信号量一般开始都初始化为1,这样第一个执行semWait的进程就可以立即进入临界区,并把s的值置为0,然后其它想进入临界区的进程都会发现0-1后为-1,为负 则挂起进程 每个都不能成功进入的进程都执行一次s-1.若离开时则s+1 这里就是所谓的互斥了.