进程基本概念
我说的肯定跟书上那些概念不一样,就我对进程的理解,当你附加给他代码,数据和分配给进程的资源,那么他就是一个进程,比如在你进入linux系统时,你进入的就是一个大的进程,有控制显示器的声音的键盘的什么的。这只是我的理解。
fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做相同的事,但如果初始参数或这传入的变量不同,两个进程也可以做不同的事。
一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的进程中。
示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main ()
{
pid_t fpid; //fpid表示fork函数返回的值
int count=0;
fpid=fork();
if (fpid < 0)
printf("error in fork!");
else if (fpid == 0) {
printf("我是子进程, 我的进程号PID是 %d/n",getpid());
count++;
}
else {
printf("我是父进程,我的进程号PID是 %d/n",getpid());
count++;
}
printf("统计结果是: %d/n",count);
return 0;
}
运行结果:
我是子进程,我的进程号PID是2605
统计结果是:1
我是父进程,我的进程号PID是2604
统计结果是:1
在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同,将要执行的下一条语句都是if(fpid<0)然后一直执行到结束,如果你要想某些代码在子进程运行,某些代码在父进程可以运行,那么fpid是个好东西,因为在父进程他返回的是子进程的PID在子进程返回的是0。没有创建成功返回负数。
说一下原理:
为什么两个进程的fpid不同呢,这与fork函数的特性有关。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值。
- 在父进程中,fork返回新创建子进程的进程ID
- 在子进程中,fork返回0;
- 如果出现错误,fork返回一个负值
在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中fork返回新创建子进程的进程PID。
fpid的值为什么在父子进程中不同。“其实就相当于链表,进程形成了链表,父进程的fpid(p 意味point)指向子进程的进程id, 因为子进程没有子进程,所以其fpid为0.
fork出错可能有两种原因: - 当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
- 系统内存不足,这时errno的值被设置为ENOMEM。
创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。
每个进程都有一个独特(互不相同)的进程标识符(process ID),可以通过getpid()函数获得,还有一个记录父进程pid的变量,可以通过getppid()函数获得变量的值。fork进阶
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(void)
{
int i=0;
printf("i son/pa ppid pid fpid/n");
//ppid指当前进程的父进程pid
//pid指当前进程的pid,
//fpid指fork返回给当前进程的值
for(i=0;i<2;i++){
pid_t fpid=fork();
if(fpid==0)
printf("%d child %4d %4d %4d/n",i,getppid(),getpid(),fpid);
else
printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid);
}
return 0;
}
运行结果是:
i son/pa ppid pid fpid
0 parent 2043 3224 3225
0 child 3224 3225 0
1 parent 2043 3224 3226
1 parent 3224 3225 3227
1 child 1 3227 0
1 child 1 3226 0
这个代码很有意思.
首先进入for循环,i=0,执行fork()函数,这时候就创建了一个子进程,继续往下看,这时候就有两个进程在跑这段程序了,他们共有的初始变量是i=0,但是记住这个变量是复制给子进程的。接着父子进程都要去进行if判断,如果是子进程那么if(pid==0)判断通过,当然进程间是由系统调度的,所以他们没有先后顺序,按照我分析的来,所以输出0 child 3224 3225 0第一个说明他是被3224进程创建的,第二个说明他本身的进程号是3225,第三个说明他没有子进程fpid=0,父进程pid==0是不通过的那么他会执行else,输出0 parent 2043 3224 3225同样的2043是它的父进程,3224是当前进程,3225是他创建的子进程,你们可以看到,子进程输出的父进程是3224,那么他们就是这个关系。
接着他们会同时执行第二次循环,这是i=1,那么父进程又要开一个子进程,而原来由父进程开出来的子进程,现在也即将要变成父进程,开出一个子进程,是不是有点乱了,呜呜。我画个图吧~1
2
3
4
5
6
7 父进程
/ \
/ \
父进程 子进程
/ \ / \
/ \ / \
父 子子(父)子
用bash画的,可能不太好看,简单说一下吧!这每一层代表的是每一个阶段的状态,并不是父进程创建了父进程和子进程,下面同样的。
然后就按照这个说吧!现在父进程再开个子进程,就是最后一行的第一个子进程,同时那个要变成父进程的子进程也开了一个子进程。这时候就有四个进程在运行了,这里就有两种情况了,如果两个父进程先执行完,父进程就会死亡,那么子进程里面的父进程PID号怎么办呢?这时候子进程的父进程就会变为1.先执行两个父进程,那么就先输出1 parent 2043 3224 3226这个是最老的进程,因为他的父进程就是创建他的进程。而3224是他的进程号,3226是目前创建的子进程,而原来那个子进程已经不需要他去标识了。第二个父进程就是原来的子进程,输出的是1 parent 3224 3225 3227他的父进程的PID还是在的为3224,3225是他的进程号ID,而3227是他目前创建的新进程PID作为标识,而这时候会有进程的死亡,所以两个子进程的父进程PID变为1,输出1 child 1 3227 0 1 child 1 3226 0
我们可以用链表来表示那个四层关系的
2043->3224->3225->3227
在p3224和p3225执行完第二个循环后,main函数就该退出了,也即进程该死亡了,因为它已经做完所有事情了。p3224和p3225死亡后,p3226,p3227就没有父进程了,这在操作系统是不被允许的,所以p3226,p3227的父进程就被置为p1了,p1是永远不会死亡的
这个程序总共产生了三个子进程,执行了6此printf操作。这基本就是子父进程的创建和应用。
简单说一下getpid(),getppid(),fork()函数返回值
getpid()返回当前进程的PID,getppid() 放回当前进程的父进程的PID,fork()会返回两个值,在父进程返回他创建的子进程的pid,在子进程返回0,这就解决的不管你是什么进程创建的子进程,我都能唯一的去识别两者的关系。
进程间的互斥和同步
1、进程之间有时候会对同一个数据进行操作,或许这个数据是两个进程都要修改的(互斥),或许这个数据是一个触发点(同步)。
进程互斥是进程之间发生的一种间接性作用,一般是程序不希望的。通常的情况是两个或两个以上的进程需要同时访问某个共享变量。我们一般将发生能够问共享变量的程序段称为临界区。两个进程不能同时进入临界区,否则就会导致数据的不一致,产生与时间有关的错误。解决互斥问题应该满足互斥和公平两个原则,即任意时刻只能允许一个进程处于同一共享变量的临界区,而且不能让任一进程无限期地等待。
进程同步是进程之间直接的相互作用,是合作进程间有意识的行为,典型的例子是公共汽车上司机与售票员的合作。只有当售票员关门之后司机才能启动车辆,只有司机停车之后售票员才能开车门。司机和售票员的行动需要一定的协调。同样地,两个进程之间有时也有这样的依赖关系,因此我们也要有一定的同步机制保证它们的执行次序。
2、根据信号量来做进程间的同步和互斥。
最典型的就是PV操作:
PV操作是由P操作原语与V操作原语组成,对信号量进程操作。
互斥PV操作:
P(S) ①将互斥的信号量的值S减1(一般取变量名为S,因为S痛sign)
②如果S==0,执行该进程,否则该进程就进入等待状态,排入等待队列。
V(S) ①将信号量S=S+1
②如果S>0,执行该进程,否则释放队列中第一个等待信号量的进程。
科普一下:什么是信号量?
信号量的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。当它的值大于0时,表示当前可用资源的数量;当它的值小于0时,其绝对值表示等待使用该资源的进程个数。
接下来我又要吐槽一下了,一般网上的人都说信号量S=0时,S表示可用资源什么的。其实是这样的,互斥的信号量S初始一般为1的,他要运行临界区的时候,应该是先S–再判断,如果没有执行,则他再返回S++,这就是为什么S初始信号量为1,而判断的是S==0。
后面我会举一个实例。
使用PV操作实现进程互斥需要注意的是:
(1)每个程序中用户实现互斥的P、V操作必须成对出现,先做P操作,进临界区,后做V操作,出临界区。若有多个分支,要认真检查其成对性。
(2)P、V操作应分别紧靠临界区的头尾部,临界区的代码应尽可能短,不能有死循环。
(3)互斥信号量的初值一般为1。
同步PV操作:
PV操作是典型的同步机制之一。用一个信号量与一个消息联系起来,当信号量的值为0时,表示期望的消息尚未产生;当信号量的值非0时,表示期望的消息已经存在。用PV操作实现进程同步时,调用P操作测试消息是否到达,调用V操作发送消息。
这个跟互斥差不多,只不过这个初始值为0,这些我觉得都是可控的,只不过一般来说,你一辆车一开始肯定是空的,这个视情况而定吧。
使用PV操作实现进程同步需要注意的是:
(1)分析进程间的制约关系,确定信号量种类。在保持进程间有正确的同步关系情况下,哪个进程先执行,哪些进程后执行,彼此间通过什么资源(信号量)进行协调,从而明确要设置哪些信号量。(2)信号量的初值与相应资源的数量有关,也与P、V操作在程序代码中出现的位置有关。
(3)同一信号量的P、V操作要成对出现,但它们分别在不同的进程代码中。
举一个我觉得理解起来还不错的例子吧。先上代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
int spys=1;//售票员的私有信号量
int spy;//选择售票员的人数,最多只能有两个
int ck=0;//厅内购票者私有信号量
int n=MAX;
int x;//每次进入售票厅内的人数
int P1();//定义P操作函数,一个售票员执行的过程
int P2();//定义p操作,两个售票员执行该过程
int j=0;//人多时,减少的人数
int V1();//V操作函数,一个售票窗口时购买者执行的过程
int V2();//V操作函数,两个售票窗口时购买者执行的过程
void Hello();//输出客套语句
void Introduce();//本客运站人员介绍
int main()
{
Introduce();//初始化控制台颜色
printf("请输入今天上班的售票员人数:(最多2位):\n");
scanf("%d",&spy);//输入售票员人数
switch(spy){
case 0:{
printf("sorry!由于今天假日,所以售票员不上班,请各位乘客改乘其他交通工具!\n");
break;
}
case 1:{
printf("今天就一个窗口售票哦!请乘客们排成一队!谢谢合作!\n");
P1();
break;
}
case 2:{
printf("今天有两个售票窗口啦!请乘客们按顺序排成两队!谢谢合作\n");
P2();
break;
}
default:{
printf("本站员工有限,没有多余员工哦!");
break;
}
}
return 0;
}
int P1() //一个售票窗口时执行的过程
{
spys--;//p操作改变售票员的信号量
if(spys==0)
{
Sleep(2000);
printf("20B506客运站欢迎各位乘客来乘坐本公司的汽车!祝你旅途愉快!\n");
Sleep(2000);
n--;
Hello();
Sleep(3000);
printf("当前购买者完成购票,请下一位购票者就绪\n");
Sleep(2000);
printf("当前售票厅内人数为%d\n",n);
ck++;//v操作改变厅内购票者信号量
}
else
spys++;
V1();
return 0;
}
int V1()//顾客的执行过程
{
ck--;//改变顾客进入厅内的信号量操作
if(ck==0)
{
printf("请厅外的购票者排按顺序进入购票厅内(最多能进入人数为:%d):\n",MAX-n);
scanf("%d",&x);
n=n+x;
printf("售票厅内人数为%d",n);
if(n>MAX){
printf("人数太多了,站不住脚呀!请出去一些人到售票厅外等候吧!\n");
printf("请输入出去的人数:\n");
scanf("%d",&j);
while(j>n-MAX){
printf("还可以再进来些人哦!!");
printf("请在厅外等候的乘客进入售票厅内吧:\n");
scanf("%d",&j);
}
n=n-j;
printf("售票厅内人数为%d",n);
}
if(n==0) {
printf("可以下班了,售票员们,你们辛苦了\n");
return 0;
}
}
//printf("\n厅内排队人数为%d,请厅外购票者们耐心等候,谢谢合作\n",n-1);
if(n>=20)
printf("已经达到最大人数,请厅外的乘客耐心等候!谢谢合作\n");
else
printf(",本售票厅内可容纳最多20人数!,还可再进入%d人。\n",MAX-n);
Sleep(2000);
spys++;//改变售票员的信号量
P1();
return 0;
}
int P2() //两个售票窗口时执行的过程
{
spys--;//改变售票员的信号量
if(spys==0)
{
Sleep(2000);
printf("20B506客运站欢迎各位乘客来乘坐本公司的汽车!祝你旅途愉快!\n");
Sleep(2000);
n=n-2;
Hello();
Sleep(3000);
printf("当前购买者完成购票,请下一位购票者就绪\n");
Sleep(2000);
printf("当前售票厅内人数为%d\n",n);
ck++;//改变购票者的信号量
}
else
spys++;
V2();
return 0;
}
int V2()//顾客的执行过程
{
ck--;//相当于p操作
if(ck==0)
{
printf("请厅外的购票者排按顺序进入购票厅内(最多能进入人数为:%d):\n",MAX-n);
scanf("%d",&x);
n=n+x;
printf("售票厅内人数为%d",n);
if(n>MAX){
printf("人数太多了,站不住脚呀!请出去一些人到售票厅外等候吧!\n");
printf("请输入出去的人数:\n");
scanf("%d",&j);
while(j>n-MAX){
printf("还可以再进来些人哦!!");
printf("请在厅外等候的乘客进入售票厅内吧:\n");
scanf("%d",&j);
}
n=n-j;
printf("售票厅内人数为%d",n);
}
if(n==0) {
printf("可以下班了,售票员们,你们辛苦了\n");
return 0;
}
}
//printf("\n厅内排队人数为%d,请厅外购票者们耐心等候,谢谢合作\n",n-1);
if(n>=20)
printf("已经达到最大人数,请厅外的乘客耐心等候!谢谢合作\n");
else
printf(",本售票厅内可容纳最多20人数!,还可再进入%d人。\n",MAX-n);
Sleep(2000);
spys++;//执行v操作
P2();
return 0;
}
void Hello()//say hello
{
printf("售票厅内能容纳最多的人数为20人,请乘客们按顺序在厅外等候!谢谢合作!\n");
}
void Introduce()
{
system("color 4e");
printf("------------------------------欢迎来到20B506客运站-----------\n");
printf("-----站长:allen-----\n");
printf("----------副站长:vivien----\n");
printf("--------------售票员甲:jack ------\n");
printf("-------------------售票员乙:mike-----\n");
printf("----------------------------------我们的服务就是最好的承诺!\n");
printf("\n");
printf("\n");
printf("\n");
printf("\n");
printf("\n");
}
我们从这个里面分析同步和互斥的PV操作。在p1操作里面有一个–有一个++,很明显一个是控制互斥操作的信号量,一个是控制同步操作的信号量。那我再看看他们是控制那段临界区,而这段临界区完成了又改变了什么,我们就可以很清楚的了解到。
那我们总结如下:
存在互斥关系,一个售票员只能对应一个购票者,当售票员正在售票时,其他不能购票。
存在同步关系,当厅内购票者人满时,只有售票员售票完成之后厅外的人才能进来。
信号量如何体现呢?
Sys信号量控制售票员是否被购票者占用若正在被占用时sys=0,初始为sys=1,下一个无法进行购票,当购票完成后sys++,则又能进行购票。
Ck信号量控制,ck初始为0,售票是否完成,若完成++1,则厅外的人才能进来,进来后又—1.
这就是同步互斥的PV操作。
管程
信号量机制功能强大,但使用时对信号量的操作分散,不好控制,读写和维护都很困难。就像你做一个项目一样,没有一个良好的框架,东写一点,西写一点最后不知道变量是哪的哪的,难道最后再合成同一个文件的代码吗?当然不会。所以后来又提出了一种集中式同步进程–管程。其基本思想是将共享变量和对它们的操作集中在一个模块中,操作系统或并发程序就由这样的模块构成。这样模块之间联系清晰,便于维护和修改,易于保证正确性。
管程作为一个模块,定义如下:
monitor_name=MoNITOR;
共享变量说明;
define 本管程内部定义、外部可调用的函数名表;
use 本管程外部定义、内部可调用的函数列表;
内部定义的函数说明和函数体
{
共享变量初始化语句;(这就是我前面说的初始量都是视情况而定的)
}
管程的特性:
(1)模块化。管程是一个基本程序单位,可以单独编译;
(2)抽象数据类型。管程是种不仅有数据,而且有对数据的操作。
(3)信息掩蔽。管程外可以调用管程内部定义的函数,但函数的具体实现外部不可见;对于管程中定义的共享变量的所有操作都局限在管程中,外部只能通过调用管程的某些函数来间接访问这些变量。因此管程有很好的封装性。
为了保证共享变量的数据一致性,管程应互斥使用。管程通常是用于管理资源的,因此管程中有进程等待队列和相应的等待和唤醒操作。在管程入口有一个等待队列,称为入口等待队列。当一个已进入管程的进程等待时,就释放管程的互斥使用权;当已进入管程的一个进程唤醒另一个进程时,两者必须有一个退出或停止使用管程。在管程内部,由于执行唤醒操作,可能存在多个等待进程(等待使用管程),称为紧急等待队列,它的优先级高于入口等待队列。
因此,一个进程进入管程之前要先申请,一般由管程提供一个enter过程;离开时释放使用权,如果紧急等待队列不空,则唤醒第一个等待者,一般也由管程提供外部过程leave。
管程内部有自己的等待机制。管程可以说明一种特殊的条件型变量:var c:condition;实际上是一个指针,指向一个等待该条件的PCB队列。对条件型变量可执行wait和signal操作:(联系P和V; take和give)
wait(c):若紧急等待队列不空,唤醒第一个等待者,否则释放管程使用权。执行本操作的进程进入C队列尾部;
signal(c):若C队列为空,继续原进程,否则唤醒队列第一个等待者,自己进入紧急等待队列尾部。
管程实例一:
生产者消费者问题。生产者进程将产品放入某一缓冲区,消费者进程到此缓冲区中取产品。这个过程必须保证:1. 当缓冲区有剩余空间时,生产者才能在其中放入产品;2. 当缓冲区有数据时,消费者才能在其中取出产品。
解决方案:使用管程机制来实现生产者和消费者之间的同步互斥问题
1.假设有一基本管程monitor,提供了enter、leave、signal、wait等操作;
2.条件变量notfull表示缓冲区不满,条件变量notempty表示缓冲区不空;
3.缓冲区buff[0…n-1]用来存放产品,最大可放n件产品;
4.定义整型变量count表示缓冲区当前的产品数,指针in指向缓冲区当前第一个空的位置,指针out指向缓冲区当前第一个不空的位置;
5.定义过程add(ItemType item)1
2
3
4
5
6
7
8
9
10add(ItemTypeitem) //生产者进程在缓冲区放入产品
{
if(count==n) wait(notfull);
//如果此时缓冲区已满,那么进程必须等待notfull,这意味着进程已经被阻塞到紧急队列里
buff[in]=item; //否则在第一个空的位置放入产品
in=(in+1)%n; //指针循环加1
count++;
signal(notempty);
//此时缓冲区已经多了一个产品,也就是说生产者进程去唤醒因取不到产品被阻塞的消费者进程
}
6.定义过程ItemType remove()1
2
3
4
5
6
7
8
9
10ItemType remove() //消费者进程在缓冲区取出产品
{
if(count==0) wait(notempty);
//如果缓冲区没有产品,那么消费者必须等待notempty,也就是被阻塞到紧急队列中去
item=buff[out]; //消费者从第一个不空的位置取出产品
out=(out+1)%n;
signal(notfull);
//此时缓冲区多了一个空的单元,也就是消费者进程去唤醒因缓冲区已满而不能放入产品的生产者进程
return item;
}
7.生产者进程代码段1
2
3
4
5
6
7while(true)
{
produce(&item); //生成出一件产品
monitor. enter(); //进入管程
monitor. add(); //调用add方法,放入产品
monitor. leave(); //离开管程
}
8.消费者进程代码1
2
3
4
5
6
7while(true)
{
monitor. enter();
item=monitor. remove(); //取出产品
monitor. leave();
consumer(&item); //进行消费
}
管程实例二:
读者—写者问题。现有一个缓冲区,有若干读者进程和若干写者进程。读者进程在缓冲区读数据,写者进程在缓冲区写入数据。这个过程必须保证:1. 读者进程之间不需要互斥;2.写者进程之间必须互斥,即当一个写者进程在缓冲区写入数据时,别的写者进程必须被阻塞;3. 读者进程和写者进程必须互斥,即当有读者进程在读数据,写者进程必须被阻塞,有写者进程在写数据时,读者进程必须被阻塞。
解决方案:采用管程机制来解决读者—写者问题
1.假设已经有一个基本管程Monitor提供了enter、leave、signal、wait等操作;
2.定义条件变量r表示可以对缓冲区读,条件变量w表示可以对缓冲区写;
3.定义布尔类型变量IsWriting表示当前有写者进程在缓冲区写数据;
4.整型变量read_count表示读数据的个数;
5.定义过程startRead()1
2
3
4
5
6
7void startRead()
{
if(IsWriting) wait(r);
//此时缓冲区有写者进程在写数据,那么读者进程等待r,也就是读者进程被阻塞到紧急队列中
read_count++; //否则,读出数据
signal(r); //唤醒被阻塞的读者进程
}
6.定义过程endRead()1
2
3
4
5
6void endRead()
{
read_count--;
if(read_count= =0) signal(w);
// 此时表示所有读者进程都已经读完数据,那么唤醒被阻塞的写 者进程
}
7.定义过程startWrite1
2
3
4
5
6void startWrite()
{
if(read_count!=0 || IsWriting) wait(w);
//此时表示如果有读者进程存在或者其他写者进程存在,那么将要写数据的写者进程被阻塞
IsWriting=true;
}
8.定义过程endWrite()1
2
3
4
5
6
7void endWrite()
{
IsWriting=false;
if(r!=null) signal(r);
//如果有读者进程存在,那么唤醒读者进程
else signal(w); //否则唤醒写者进程
}
9.读者进程代码段1
2
3
4
5
6
7
8while(true)
{
Monitor. enter();
Monitor. startRead();
read();
Monitor. endRead();
Monitor. leave();
}
10.写者进程代码段1
2
3
4
5
6
7
8while(true)
{
Monitor. enter();
Monitor. startWrite();
write();
Monitor. endWrite();
Monitor. leave();
}
这就是管程。