目录
一、问题描述
有一群生产者进程在生产产品,并将这些产品提供给消费者进程去消费。为使生产者进程与消费者进程能并发执行,在两者之间设置了一个具有 n 个缓冲区的缓冲池,生产者进程将其所生产的产品放入一个缓冲区中;消费者进程可从一个缓冲区中取走产品去消费。
二、问题分析
1. 实现数据存取
使用数组作为缓冲池,容量为 n 。设置 in 和 out 两个指针,分别指向:
- 生产者将要存放数据的存储单元
- 消费者将要取出数据的存储单元
并且将数组组织成循环缓冲的形式。
/*指针但实际是数组下标*/
int in = 0;
int out = 0;
/*每次需要修改指针时*/
in = (in + 1) % n;
out = (out + 1) % n;
可将上述信息描述如下图:
2. 实现互斥与同步
根据实际需要,可以将信号量分为:
- 互斥型信号量:S=1,即该资源只有一个,是临界资源
- 资源型信号量:S≥1,即该资源不止一个
对于临界资源,我们必须保证对其访问的唯一性。因此需要将其的信号量初始值设置为 1,且将其置于临界区(critical section)中,即 wait() 和 signal() 操作之间。
在生产者-消费者问题中,我们设置三个信号量:
semaphore mutex, empty, full;
mutex 用于控制缓冲池的使用权,empty 用于指示缓冲池中空缓冲区的个数,full 用于指示缓冲池中满缓冲区的个数。空缓冲区就是未放入数据的存储单元,满缓冲区就是已放入数据的存储单元。个人认为,这两个名字取得好离谱,很容易将其与缓冲池的空和满混淆。
由于任一时刻,缓冲池的使用权只能被一个进程拥有,因此缓冲池信号量应该是一个互斥型信号量。由于空缓冲区和满缓冲区的数量可以是 0 ~ n 之间的任意值,因此它俩的信号量应该是资源型信号量。定义信号量如下:
semaphore mutex, empty, full;
mutex.value = 1; //由于缓冲池是一种临界资源,因此使用互斥型信号量
empty.value = n; //代表空缓冲区的数量,最初存储单元均为空
full.value = 0; //代表满缓冲区的数量,最初存储单元均为空
这两种信号量的区别就在于:
- 初始值是否为 1
- wait() 和 signal() 是否需要成对出现
互斥型信号量需要同时满足上述两个条件。
3. 执行流程
生产者-消费者执行流程图
三、解决方法
1. 利用记录型信号量解决生产者-消费者问题
变量定义、所需函数及主程序如下:
semaphore 变量使用 . 访问成员,semaphore 指针使用 -> 访问成员。
typedef struct {
int value;
struct process_control_block * list;
} semaphore;
int in = 0;
int out = 0;
semaphore mutex, empty, full;
mutex.value = 1; //由于缓冲池是一种临界资源,因此使用互斥型信号量
empty.value = n; //代表空缓冲区的数量,最初存储单元均为空
full.value = 0; //代表满缓冲区的数量,最初存储单元均为空
void producer(); //生产者进程
void consumer(); //消费者进程
void main() {
cobegin //并发执行
producer(); consumer();
coend
}
wait() 和 signal() 描述如下:
wait(semaphore * S) {
S->value--;
if(S->value < 0) block(S->list);
//S->value == 0时代表刚好取走最后一个资源
}
signal(semaphore * S) {
S->value++;
if(S->value <= 0) wakeup(S->list);
//S->value == 0时代表刚好还有一个进程被阻塞
}
生产者进程如下:
void producer() {
do {
//produce an item in nextp
wait(&empty); //判断是否有空存储单元
wait(&mutex); //判断是否可用缓冲池
buffer[in] = nextp;
in = (in + 1) % n;
signal(&mutex); //唤醒一个使用者
signal(&full); //唤醒一个消费者
} while(true);
}
消费者进程如下:
void producer() {
do {
wait(&full); //判断是否有空存储单元
wait(&mutex); //判断是否可用缓冲池
nextc = buffer[out];
out = (out + 1) % n;
signal(&mutex); //唤醒一个使用者
signal(&empty); //唤醒一个消费者
//consume the item in nextc
} while(true);
}
1)wait(&mutex) 必须放在 wait(&full) 后面,避免抢占到了缓冲池的使用权,结果发现没有空存储单元可用,从而导致资源浪费。
2)mutex 是互斥型信号量,因此对其的 wait 和 signal 操作必须成对出现。
3)进程一旦被阻塞,不管这个时间片是否用完,都立即进入下一个时间片。
plus. 举例说明
t1 | t2 | t3 | t4 | t5 | t6 | t7 | t8 | t9 | |
running | c1: wait(full) | p1: item->nextp p1: wait(empty) p1: wait(mutex) | p2: item->nextp p2: wait(empty) p2: wait(mutex) | p3: item->nextp p3: wait(empty) | p1: CS p1: signal(mutex) p1: signal(full) finish | p2: CS p2: signal(mutex) p2: signal(full) finish | c1: wait(mutex) c1: CS | c1: signal(mutex) c1: signal(empty) finish | p3: wait(mutex) p3: CS p3: signal(mutex) p3: signal(full) finish |
full.value | 0-1=-1(<0?) | -1+1=0(<=0?) | 0+1=1(<=0?) | 1+1=2(<=0?) | |||||
full.list | {c1} | wakeup(full.list) | {} | {} | |||||
empty.value | 2-1=1(<0?) | 1-1=0(<0?) | 0-1=-1(<0?) | -1+1=0(<=0?) | |||||
empty.list | {} | {} | {p3} | wakeup(empty.list) | |||||
mutex.value | 1-1=0(<0?) | 0-1=-1(<0?) | -1+1=0(<=0?) | 0+1=1(<=0?) | 1-1=0(<0?) | 0+1=1(<0?) | 1-1=0(<0?) 0+1=0(<=0?) | ||
mutex.list | {} | {p2} | wakeup(mutex.list) | {} | {} | {} | {} | ||
ready | {p1, p2, p3} | {p2, p3, p1} | {p3, p1} | {p1} | {p2, c1} | {c1} | {} | {p3} | {} |
2. 利用 AND 型信号量解决生产者-消费者问题
变量定义如下:
int main() {
// 定义变量
typedef struct {
int value;
struct process_control_block * list;
} semaphore;
typedef struct {
/* data */
} item;
int in = 0;
int out = 0;
item buffer[n];
semaphore mutex, empty, full;
mutex.value = 1;
empty.value = n;
//...
}
Swait() 和 Ssignal() 操作描述如下:
Swait(S1, S2, ... , Sn) {
while(true) {
if(S1 >= 1 && S2 >= 1 && ... && Sn >= 1) {
for(i = 1; i <= n; ++i) Si = Si - 1;
break; //分配完资源后,直接退出while循环
} else {
/*place the process in the waiting queue associated with
the first Si found with Si<1, and set the program count
of this process to the beginning of Swait operation*/
}
}
}
Ssignal(S1, S2, ... , Sn) {
for(i = 1; i <= n; ++i) Si = Si + 1;
/*remove all the process waiting in the queue
associated with Si into the ready queue*/
}
生产者进程如下:
void producer() {
do {
//produce an item in nextp
Swait(empty, mutex); //此处讲究顺序
buffer[in] = nextp;
in = (in + 1) % n;
Ssignal(mutex, full); //此处讲究顺序
} while(true);
}
消费者进程如下:
void consumer() {
do {
Swait(full, mutex); //此处讲究顺序
nextc = buffer[out];
out = (out + 1) % n;
Ssignal(mutex, empty); //此处讲究顺序
//consume the item in nextc
} while(true);
}