第五章:进程同步互斥典型模型及管程
1、生产者/消费者模型
1.1、问题描述
系统中有一组生产者进程和一组消费者进程,生产者进程每次生产一个产品放入缓冲区,消费者进程每次从缓冲区中取出一个产品并使用。【注:这里的产品理解为某种数据】
- 生产者、消费者共享一个初始为空、大小为n的缓冲区。
- 只有缓冲区没满时,生产者才能将产品放入缓冲区,否则必须等待。
- 只有缓冲区不为空时,消费者才能从中取出产品,否则必须等待。
- 缓冲区是临界资源,各进程必须互斥地访问,即两个进程不能同时进行读写操作
1.2、问题分析
如何用信号量机制(P、V操作)实现生产者、消费者进程的这些功能呢?
我们知道信号量机制可实现互斥、同步、对一类系统资源的申请和释放,如下
- 互斥:设置初值为1的互斥信号量
- 同步:设置初值为0的同步信号量(实现一前一后”)
- 对一类系统资源的申请和释放:设置一个信号量,初始值即为资源的数量(本质上也属于“同步问题”,若无空闲资源,则申请资源的进程需要等待别的进程释放资源后才能继续往下执行)
该问题中出现的主要的两种关系:
-
生产者—消费者之间的同步关系表现为: 一旦缓冲池中所有缓冲区均装满产品时,生产者必须等待消费者提供空缓冲区;一旦缓冲池中所有缓冲区全为空时,消费者必须等待生产者提供满缓冲区。
-
生产者—消费者之间还有互斥关系: 由于缓冲池是临界资源,所以任何进程在对缓冲区进行存取操作时都必须和其他进程互斥进行。
1.3、如何实现
在生产者一消费者问题中,信号量具有两种功能。其一,它是跟踪资源的计数器;其二,它是协调生产者和消费者之间的同步器。消费者通过在一个表示满缓冲区数目的信号量上做P操作来消耗一个资源,而生产者通过在同信号灯上做V操作表示生产一个资源。在这种信号量的P、V操作实施中,计数在每次P操作后减1,而在每次V操作中增1。这一计数器的初始值是可利用的资源数目。当资源不可利用时,将申请资源的进程挂到该等待队列中。如果有一个资源释放,在等待队列中的第一个进程将被唤醒并得到资源的控制权。
PV操作题目分析的步骤:
1.关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。
2.整理思路。根据各进程的操作流程确定PV操作的大致顺序。即生产者每次要消耗(P)一个空闲缓冲区,并生产(V)一个产品。 消费者每次要消耗(P)一个产品并释放一个空闲缓冲区(V),往缓冲区放入/取走产品需要互斥
3.设置信号量。设置需要的信号量,并根据题目条件确定信号量的个数和初值。(互斥信号量初值一般为1,同步信号量的初值需要看对应资源的初始值是多少)
在这里:
semaphore mutex = 1; // 互斥信号量,实现对缓冲区的互斥访问
semaphore empty = n; // 同步信号量,表示空闲缓冲区的数量
semaphore full = 0; // 同步信号量,表示产品的数量,即非空缓冲区的数量
具体代码
producer(){
while(1){
生产一个产品;
P(empty); // 消耗一个空闲缓冲区,保证没有在同一个放入产品
P(mutex); // 实现互斥访问
把产品放入缓冲区;
V(mutex); // 实现互斥访问
V(full); // 增加一个产品
}
}
consumer(){
while(1){
P(full); // 消耗一个非空缓冲区(产品)
P(mutex); // 实现互斥访问
从缓冲区取出一个产品;
V(mutex); // 实现互斥访问
V(empty); // 空出一个缓冲区
使用产品
}
}
1.4、思考
从上图知道,互斥的实现是在同一个进程中进行的一对PV操作。同步的实现是在两个进程中进行的,在一个进程中执行P操作,在另一个进程中执行V操作。能否改变相邻PV操作的顺序?如下左图所示:
- 若此时缓冲区内已经放满产品,则empty=0, full=n。
- 则生产者进程执行1使mutex变为0,再执行2,由于已没有空闲缓冲区,因此生产者被阻塞。
- 由于生产者阻塞,因此切换回消费者进程。消费者进程执行③,由于mutex为0, 即生产者还没释放对临界资源的“锁”,因此消费者也被阻塞。
- 这就造成了生产者等待消费者释放空闲缓冲区,而消费者又等待生产者释放临界区的情况,生产者和消费者循环等待被对方唤醒,出现“死锁”
- 同样的,若缓冲区中没有产品,即full=O,empty=n。 按341的顺序从消费者进程执行也会发生死锁。
- 因此,实现互斥的P操作一定要在实现同步的P操作之后。
- 而V操作不会导致进程阻塞,因此两个V操作顺序可以交换。
我们观察上面的代码,生产者生产产品和消费者生成产品和使用产品这两个操作都是放在各自进程的PV操作之外的,那么能不能放在各自的PV操作之内呢?即放入临界区。其实从逻辑上来说是可以的,比如从缓冲区取出一个产品之后立即使用这个产品,但是这样就会造成临界区的代码量变大,消费者进程访问临界区将会耗费更多的时间,若此时有别的进程想要访问临界区是会被阻塞的,若将这些不是很非代码也放入临界区,会造成进程的并发度降低。
1.5、总结
- 互斥的是缓冲区,同步的是空闲缓冲区资源和产品。
- 知道:
- 对于producer:先P(empty),再P(mutex),再V(mutex),再V(full).
- 对于consumer:先P(full),再P(mutex),再V(mutex),再V(empty).
可以这样理解:mutex是相当于对缓冲区(临界资源)的上锁,锁完后要马上释放,所以mutex的PV操作一定在最里层。
生产者消费者问题是一个互斥、同步的综合问题。对于初学者来说最难的是发现题目中隐含的两对同步关系。有时候是消费者需要等待生产者生产,有时候是生产者要等待消费者消费,这是两个不同的“一前一后问题”,因此也需要设置两个同步信号量。
2、多生产者、多消费者模型
2.1、问题描述
桌子上有一个盘子,每次只能向其中放入一个水果。爸爸专向盘子放入苹果,妈妈专门放入橘子,儿子专等着吃盘子中的橘子,女儿专等着吃盘子中的苹果。只有盘子为空时,爸爸或妈妈才可向盘子中放入一个水果。仅当盘子中有自己需要的水果时,儿子或女儿可以从盘子中取出水果。【多:多类别】
2.2、问题分析
- 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。
- 互斥关系:(mutex= 1):对缓冲区(盘子)的访问要互斥地进行
- 同步关系(一前一后)
- 父亲将苹果放入盘子后,女儿才能取苹果
- 母亲将橘子放入盘子后,儿子才能取橘子
- 只有盘子为空时,父亲或母亲才能放入水果
- “盘子为空”这个事件可以由儿子或女儿触发,事件发生后才允许父亲或母亲放水果
- 整理思路。根据各进程的操作流程确定PV操作的大致顺序。
- 互斥:在临界区前后分别PV
- 同步: 前V后P
- 设置信号量。设置需要的信号量,并根据题目条件确定信号量的初值。(互斥信号量初值一般为1,同步信号量的初值需要看对应资源的初始值是多少)
2.3、代码实现
semaphore mutex = 1;// 实现互斥访问盘子(缓冲区)
semaphore apple = 0;// 盘子中有几个苹果
semaphore orange = 0;// 盘子中有几个橘子
semaphore plate = 1;// 盘子中还可以放入多少个水果
dad(){
while(1){
准备一个苹果;
P(plate);
P(mutex);
把苹果放入盘子;
V(mutex);
V(apple);
}
}
mom(){
while(1){
准备一个橘子;
P(plate);
P(mutex);
把橘子放入盘子;
V(mutex);
V(orange);
}
}
daughter(){
while(1){
P(apple);
P(mutex);
从盘子中取出苹果;
V(mutex);
V(plate);
吃掉苹果;
}
}
son(){
while(1){
P(orange);
P(mutex);
从盘子中取出橘子;
V(mutex);
V(plate);
吃掉橘子;
}
}
2.4、思考:
可不可以不用互斥信号量,并把对该信号量的pv操作都去掉
分析:刚开始,儿子、女儿进程即使上处理机运行也会在P操作的时候被阻塞【对应信号量值为0】。如果刚开始是父亲进程先上处理机运行,则:过程为:
- 父亲P(plate),可以访问盘子
- 切换到母亲P(plate)进程,阻塞等待盘子
- 父亲放入苹果V(apple),女儿进程被唤醒,其他进程即使运行也都会阻塞,暂时不可能访问临界资源(盘子)
- 女儿P(apple),访问盘子,V(plate), 等待盘子的母亲进程被唤醒
- 母亲进程访问盘子( 其他进程暂时都无法进入临界区) .....
结论: 即使不设置专门的互斥变量mutex,也不会出现多个进程同时访问盘子的现象,原因在于本题中的缓冲区大小为1,在任何时刻,apple、orange、plate三个同步信号量中最多只有一个是1。因此在任何时刻,最多只有一个进程的P操作不会被阻塞,并顺利地进入临界区。简而言之,当一个进程可以运行时,其他进程不满足运行的条件,所以不会出现多个进程同时访问盘子的情况,就不需要互斥变量了。
但是,如果盘子缓冲区plate大小为2,那么父亲和母亲进程都满足条件,如果没有mutex,就会两个进程同时访问缓冲区。所以,就必须专门设置一个互斥信号量mutex。因此如果缓冲区大小为1,有可能不用互斥信号量。所以还是都用互斥信号量稳一点。
2.5、总结
在生产者~消费者问题中,如果缓冲区大小为1,那么有可能不需要设置互斥信号量就可以实现互斥访问缓冲区的功能。当然,这不是绝对的,要具体问题具体分析。
建议:在考试中如果来不及仔细分析,可以加上互斥信号量,保证各进程一定会互斥地访问缓冲区。但需要注意的是,实现互斥的P操作一定要在实现同步的P操作之后,否则可能引起“死锁”。
解决“多生产者——多消费者问题”的关键在于理清复杂的同步关系。在分析同步问题(一前一后问题)的时候不能从单个进程行为的角度来分析,要把“一前一后”发生的事看做是两种“事件”的前后关系。
比如,如果从单个进程行为的角度来考虑的话,我们会有以下结论:
- 如果盘子里装有苹果,那么一定要女儿取走苹果后父亲或母亲才能再放入水果
- 如果盘子里装有橘子,那么一定要儿子取走橘子后父亲或母亲才能再放入水果
这么看是否就意味着要设置四个同步信号量分别实现这四个“一前一后”的关系了?正确的分析方法应该从“事件”的角度来考虑,我们可以把上述四对“进程行为的前后关系”抽象为一对“事件的前后关系”
- 即:盘子变空事件一定要发生在放入水果事件之前。
- “盘子变空事件”既可由儿子引发,也可由女儿引发;
- “放水果事件”既可能是父亲执行,也可能是母亲执行。
- 这样的话,就可以用一个同步信号量解决问题了
3、读写锁问题
3.1、问题描述
有读者和写者两组并发进程,共享一个文件,当两个或两个以上的读进程同时访问共享数据时不会产生副作用,但若某个写进程和其他进程(读进程或写进程)同时访问共享数据时则可能导致数据不一致的错误。因此要求:可以同时读、只能一个写、写完前不能有其他进程访问,即
- 允许多个读者可以同时对文件执行读操作;
- 只允许一个写者往文件中写信息;
- 任一写者在完成写操作之前不允许其他读者或写者工作:
- 写者执行写操作前,应让已有的读者和写者全部退出。
3.2、问题分析
-
关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。
- 两类进程:写进程、读进程
- 互斥关系:写进程——写进程、写进程一读进程。
- 读进程与读进程不存在互斥问题。
-
整理思路。根据各进程的操作流程确定P、V操作的大致顺序
- 写者进程和任何进程都互斥,设置一个互斥信号量rw,在写者访问共享文件前后分别执行P、V操作。
- 读者进程和写者进程也要互斥,因此读者访问共享文件前后也要对rw执行P、V操作。
- 如果所有读者进程在访问共享文件之前都执行P(rw)操作,那么会导致各个读进程之间也无法同时访问文件。
-
设置信号量。设置需要的信号量,并根据题目条件确定信号量初值。 (互斥信号量初值一般为1,同步信号量的初始值要看对应资源的初始值是多少)
-
Key: 读者写者问题的核心思想--怎么处理该问题呢?
-
P(rw)和V(rw)其实就是对共享文件的“加锁”和“解锁”。既然各个读进程需要同时访问,而读进程与写进程又必须互斥访问,那么我们可以让第一个访问文件的读进程“加锁”,让最后一个访问完文件的读进程“解锁”。可以设置一个整数变量count来记录当前有几个读进程在访问文件。
-
核心思想:设置了一个计数器count 用来记录当前正在访问共享文件的读进程数。
3.3、代码实现
semaphore rw = 1; // 用于实现对文件的互斥访问。表示当前是否有进程在访问进程文件。
int count = 0; // 记录当前有几个读进程在访问
writer(){
while(1){
P(rw); // 写之前“加锁”
写文件...
V(rw); // 写之后“解锁”
}
}
reader(){
while(1){
if(count == 0); P(rw); // 第一个读进程负责加锁,后续读进程不会执行这段if语句所以会执行后面读文件过程
count ++; // 记录访问文件读线程数量 + 1
V(mutex);
读文件..
count --; // 记录访问文件读进程数量 - 1
if(count == 0) V(rw); // 最后一个读进程负责解锁
V(mutex);
}
}
3.4、思考
若两个读进程并发执行,则两个读进程有可能先后执行P(rw),从而使第二个读进程阻塞的情况【得不到读锁,锁被线程1拿到了,还没有释放】。
如何解决:出现上述问题的原因在于对count变量的检查和赋值无法一气呵成,因此可以设置另一个互斥信号量mutex来保证各读进 程对count的访问是互斥的。
这样看似解决了多个读进程并发的问题,但是存在一个潜在问题:只要有读进程还在读,写进程就要一直阻塞,可能“饿死”。因此,该算法为读进程优先。
读写公平:
// 用于实现对文件的互斥访问。表示当前是否有进程在访问进程文件。
semaphore rw = 1;
int count = 0; // 记录当前有几个读进程在访问
semaphore mutex = 1; // 用于保证对count变量的互斥访问
semaphore w = 1; // 用于实现“写优先”
writer(){
while(1){
P(w);
P(rw); // 写之前“加锁”
写文件...
V(rw); // 写之后“解锁”
V(w);
}
}
reader(){
while(1){
P(w);
P(mutex); // 各读进程互斥访问count
if(count == 0); P(rw); // 第一个加锁
count ++; // 记录访问文件读线程数量 + 1
V(mutex);
V(w);
读文件..
P(mutex); // 各读线程互斥访问
count --; // 记录访问文件读线程数量 - 1
if(count == 0) V(rw); // 最后一个解锁
V(mutex);
}
}
分析以下并发执行P(w)的情况:
- 读者1→读者2
- 写者1→写者2
- 写者1→读者1
- 读者1→写者1→读者2
- 写者1→读者1→写者2
结论:在这种算法中,连续进入的多个读者可以同时读文件;写者和其他进程不能同时访问文件;写者不会饥饿,但也并不是真正的“写优先”,而是相对公平的先来先服务原则
3.5、总结
读者——写者问题为我们解决复杂的互斥问题提供了一个参考思路:
- 其核心思想在于设置了一个计数器count用来记录当前正在访问共享文件的读进程数。
- 我们可以用count的值来判断当前进入的进程是否是第一个/最后一个读进程,从而做出不同的处理。即第一个进程则执行“上锁”操作,最后一个进程则执行“解锁”操作。
- 另外,对count变量的检查和赋值不能一气呵成导致了一些错误,如果需要实现“一气呵成”,自然应该想到用互斥信号量。
- 最后,还要认真体会我们是如何解决“写进程饥饿”问题的【增加信号量w】。
- 绝大多数的PV操作大题都可以用之前介绍的几种生产者-消费者问题的思想来解决,如果遇到更复杂的问题,可以想想能否用读者写者问题的这几个思想来解决。
4、哲学家进餐问题
4.1、问题描述
一张圆桌坐着5名哲学家,每两个哲学家之间的桌上摆一根筷子,桌子中间是一碗饭。哲学家们倾注毕生的精力用于思考和进餐,哲学家在思考时,并不影响他人。只有当哲学家饥饿时,才试图拿起左、右两根筷子(一根一根拿起)。如果筷子已在他人手上,则需等待。饥饿的哲学家只有同时拿起两根筷子才开始进餐,当进餐完毕后,放下筷子继续思考。
4.2、问题分析
- 关系分析。系统中有5个哲学家进程,5位哲学家与左右邻居对其中间筷子的访问是互斥关系。
- 整理思路。这个问题中只有互斥关系,但与之前遇到的问题不同的事,每个哲学家进程需要同时持有两个临界资源才能开始吃饭。如何避免临界资源分配不当造成的死锁现象【哲学家1拿着一只筷子不放,哲学家2等着这一只筷子,死循环】,是哲学家问题的精髓。
- 信号量设置。定义互斥信号量数组chopstick[5]={1,1,1,1,1}用于实现对5个筷子的互斥访问。并对哲学家按0~4编号,哲学家i左边的筷子编号为i,右边的筷子编号为(i+1)%5。
初始代码:
semaphore chopstick[5] = {1,1,1,1,1};
Pi(){
P(chopstick[i]); // 拿左边筷子
P(chopstick[(i + 1) % 5]); // 拿右边筷子
吃饭...
V(chopstick[i]); // 放下左边
V(chopstick[(i + 1)]); // 放下右边
思考..
}
4.3、死锁解决
**上面代码看似没什么问题。但问题大了。**当5个哲学家并发拿起左边筷子,每位哲学家循环等待右边的人放下筷子(阻塞)。那就造成“死锁”。
解决方案一:
只允许四个同时进餐:可以对哲学家进程施加一些限制条件,比如最多允许四个哲学家同时进餐。这样可以保证至少有一个哲学家是可以拿到左右两只筷子的
semaphore chopstick[5] = {1,1,1,1,1};
semaphore eat = 4;
Pi(){
P(eat); // 限制四个人能同时拿筷子
P(chopstick[i]); // 拿左边筷子
P(chopstick[(i + 1) % 5]); // 拿右边筷子
V(eat);
吃饭...
V(chopstick[i]); // 放下左边
V(chopstick[(i + 1)]); // 放下右边
思考..
}
解决方案二:
基数拿 i 筷子,偶数拿 i + 1 :要求奇数号哲学家先拿左边的筷子,然后再拿右边的筷子,而偶数号哲学家刚好相反。用这种方法可以保证如果相邻的两个奇偶号哲学家都想吃饭,那么只会有中一个可以拿起第一只筷子,另一个会直接阻塞。这就避免了占有一支后再等待另一只的情况。
semaphore chopstick[5] = {1,1,1,1,1};
Pi(){
if(i % 2 == 1){
P(chopstick[i]); // 拿左边筷子
P(chopstick[(i + 1) % 5]); // 拿右边筷子
}else if(i % 2 == 0){
P(chopstick[(i + 1) % 5]); // 拿右边筷子
P(chopstick[i]); // 拿左边筷子
}
吃饭...
V(chopstick[i]); // 放下左边
V(chopstick[(i + 1)]); // 放下右边
思考..
}
解决方案三:
仅当一个哲学家左右两支筷子都可用时,才允许他拿起筷子。(互斥的拿起筷子)
semaphore chopstick[5] = {1,1,1,1,1};
semaphore mutex = 1; //互斥的拿起筷子
Pi(){ //i号哲学家的进程
while(1){
P(mutex);
P(chopstick[i]); // 拿左边筷子
P(chopstick[(i + 1) % 5]); // 拿右边筷子
V(mutex);
吃饭...
V(chopstick[i]); // 放下左边
V(chopstick[(i + 1)]); // 放下右边
思考..
}
}
假设1:哲学家0拿到了 筷子0 ,执行完P(chopstick[i]),切换到了哲学家2, 那么由于哲学家0执行了P(mutex),所以哲学家2被阻塞,直到0号哲学家顺利的拿到了 筷子1 , 再对mutex执行V操作之后,哲学家2可以被唤醒,拿起左边和右边的筷子。
假设2:哲学家0拿到了 筷子0 和 筷子1 ,正在吃饭,此时哲学家1尝试拿筷子1时,在阻塞P(chopstick[i])【筷子1被哲学家0拿着的】,然后这时候马上切换到哲学家2,他想拿到筷子, 但是哲学家1被阻塞没有执行V操作释放mutex,从而阻塞在P(mutex),所以从这种情况来看,即使2号哲学家左右两边都有筷子,但是还是无法拿起两边的筷子。
假设3:哲学家0拿起左边和右边的筷子,之后哲学家4拿起左边的筷子,但是会阻塞在右边的筷子,这时哲学家4是拿了一只筷子,在等待别的筷子。
因此这种方法并不能保证只有两边的筷子都可用时,才允许哲学家拿起筷子。所以更准确的说法应该是:各哲学家拿筷子这件事必须互斥的执行【mutex信号量的设置】。这就保证了即使一个哲学家在拿筷子拿到一半时被阻塞,也不会有别的哲学家会继续尝试拿筷子。这样的话,当前正在吃饭的哲学家放下筷子后,被阻塞的哲学家就可以获得等待的筷子了。
4.4、总结
哲学家进餐问题的关键在于解决进程死锁。
这些进程之间只存在互斥关系,但是与之前接触到的互斥关系不同的是,每个进程都需要同时持有两个临界资源,因此就有“死锁"问题的隐患。如果在考试中遇到了一个进程需要同时持有多个临界资源的情况,应该参考哲学家问题的思想,分析题中给出的进程之间是否会发生循环等待,是否会发生死锁。可以参考哲学家就餐问题解决死锁的三种思路。
5、管程
5.1、为什么要引入管程
信号量机制存在问题:编写程序困难、易出错。能不能设计一种机制,让程序员写程序时不需要再关注复杂的PV操作,让写代码更加轻松,能不能设计一种机制,让程序员写程序时不需要再关注复杂的PV操作,让写代码更轻松呢?
1973年,Brinch Hansen 首次在程序设计语言 (Pascal)中引入了“管程”成分----一种高级同步机制
5.2、管程的定义和基本特征
组成
管程是一种特殊的软件模块,有这些部分组成:类?
- 局部于管程的共享数据结构说明;
- 对该数据结构进行操作的一组过程(函数);
- 对局部于管程的共享数据设置初始值的语句;
- 管程有一个名字。
基本特征:
- 局部于管程的数据只能被局部于管程的过程所访问;
- 一个进餐只有通过调用管程内的过程才能进入管程访问共享数据;
- 每次仅允许一个进餐在管程内执行某个内部过程。
5.3、拓展1:用管程解决生产者消费者问题
引入管程的目的无非就是要更方便地实现进程互斥和同步。
需要在管程中定义共享数据(如生产者消费者问题的缓冲区) 需要在管程中定义用于访问这些共享数据的“入口”——其实就是一些函数(如生产者消费者问题中,可以定义一个函数用于将产品放入缓冲区,再定义一个函数用于从缓冲区取出产品) 只有通过这些特定的“入口”才能访问共享数据 管程中有很多“入口”,但是每次只能开放其中一个“入口”,并且只能让一个进程或线程进入(如生产者消费者问题中,各进程需要互斥地访问共享缓冲区。管程的这种特性即可保证一个时间段内最多只会有一个进程在访问缓冲区 注意:这种互斥特性是由编译器负责实现的,程序员不用关心) 可在管程中设置条件变量及等待 / 唤醒操作以解决同步问题。可以让一个进程或线程在条件变量上等待(此时,该进程应先释放管程的使用权,也就是让出“入口”);可以通过唤醒操作将等待在条件变量上的进程或线程唤醒。
封装思想:程序员可以用某种特殊的语法定义一个管程(比如: monitor ProducerConsumer …… end monitor; ),之后其他程序员就可以使用这个管程提供的特定“入口”很方便地使用实现进程同步/互斥了。
5.4、拓展2:Java中类似于管程的机制
Java 中,如果用关键字synchronized 来描述一个函数,那么这个函数同一时间段内只能被一个线程调用
public static class Monitor{
private Item [] buffer = new Item[N];
private int count = 0;//定义为私有全局变量,具体详情参考专栏java基础
public synchronized void insert(Item item){
if(count == N) this.wait();
buffer[count ++] = item;
if(count == 1) this.notify();
}
public synchronized remove(){
if(count == 0) this.wait();
count --;
if(count == N - 1) this.notify();
return buffer[count];
}
}
生产者、消费者代码
ProducerConsumer pc = new ProducerConsumer();
//生产者进程
producer(){
while(1){
item = 生产一个产品;
pc.insert(item);
}
}
//消费者进程
consumer(){
while(1){
item = pc.remove();
消费产品item;
}
}
synchronized关键字保证了每次只能有一个线程进入insert 函数,如果多个线程同时调用insert 函数,则后来者需要排队等待