code link: 欢迎star
文章目录
1. 实验目的
探索、理解并掌握操作系统同步机制的应用编程方法,针对典型的同步问题,构建基于Windows(或 Linux)操作系统同步机制的解决方案。
2. 实验内容
了解、熟悉和运用 Windows(或 Linux)操作系统同步机制及编程方法,针对典型的同步问题,譬如生产者-消费者问题、读者优先的读者-写者问题、写者优先的读者-写者问题、读者数限定的读者-写者问题、哲学家就餐问题等(任选四个即可),编程模拟实现相应问题的解决方案。
3. 实验要求
典型同步问题模拟处理编程功能设计要求为:运用 Windows(或 Linux)操作系统同步机制,从生产者-消费者问题、读者优先的读者-写者问题、写者优先的读者-写者问题、读者数限定的读者-写者问题、哲学家就餐问题等典型同步问题中选取四个同步问题,分析、设计和编程模拟实现相应问题的解决方案。
4. 实验思路
- 第一部分,生成者消费者问题。使用两个整型信号量empty和full,一个互斥控制量mutex。创建两个线程函数,read和write。这里参考了ppt上的代码,采用了一个nloop进行循环次数控制。
- 第二部分,读者优先的读者和写者问题。其中,读者可以并发执行,但是写者进程与其他进程互斥。Readcount计数变量也是临界资源,也要添加一个互斥变量,进行互斥访问。由于读写操作我们可以简化为往流中读写数据,我们可以直接使用一个string 的buffer进行读写,相比文件的读写更加快速,适用于实验的调试。
- 第三部分,写者优先的读者和写者问题。我们的目标是,写者优先,即需要有一个写者队列,注意写者仍要保持互斥,只要队列数大于0读者就不能进行;同时,读者要能进行并行读出且写者仍要互斥。相比第二部分,只要增加一个由写者队列数控制的开关即可。
- 第四部分,哲学家就餐问题。这里采用互斥信号量保证同时拿起两支筷子。同时设置一个最高nloop控制最多吃饭次数;将think和eat简化为交互内容输出,方便程序验证。
5. 实验内容
5.1. 相关环境介绍
操作系统:window 10 21H2
开发环境:Clion-2022.2.1-Windows
编译器:mwing-10.0
5.2. 生产者消费者问题
代码:
01:#define N 5
02:#define MAXNLOOP 30
03:
04:int in = 0;
05:int out = 0;
06:HANDLE WINAPI empty = CreateSemaphore(NULL, 10, 10, "empty");
07:HANDLE WINAPI full = CreateSemaphore(NULL, 0, 10, "full");
08:HANDLE WINAPI mutex = CreateMutex(NULL, FALSE, "MutexToProtectCriticalResource");
09:
10:string buffer[N];
11:int nloop = 0;
12:DWORD WINAPI ThreadExecuteZZWProducer(LPVOID lpParameter){
13: int *pID = (int *)lpParameter;
14: string nextp = to_string(*pID) + " produced";
15: while(1){
16: WaitForSingleObject(empty, INFINITE);
17: WaitForSingleObject(mutex, INFINITE);
18: if(nloop>MAXNLOOP) break;
19: buffer[in] = nextp;
20: in = (in + 1) % N;
21: cout << "nloop " << nloop << " producer " << *pID << " has produced on the buffer " << in << "\n";
22: ++nloop;
23: ReleaseMutex(mutex);
24: ReleaseSemaphore(full, 1, NULL);
25:
26: }
27: return 0;
28:}
29:
30:DWORD WINAPI ThreadExecuteZZWConsumer(LPVOID lpParameter){
31: int *pID = (int*)lpParameter;
32: string nextp;
33: while(1){
34: WaitForSingleObject(full, INFINITE);
35: WaitForSingleObject(mutex, INFINITE);
36: if(nloop>MAXNLOOP) break;
37: nextp = buffer[out];
38: out = (out +1) % N;
39: cout << "nloop " << nloop << " consumer "<< *pID << " has consumed on the buffer " << out << " the content is producer "<< nextp << "\n";
40: ++nloop;
41: ReleaseMutex(mutex);
42: ReleaseSemaphore(empty, 1, NULL);
43: };
44: return 0;
45:}
结果验证
结果符合预期,四个线程都可以参与读写,读出的内容经人工验证也是正确的。只不过可能考虑到运行速度过快,出现过buffer数太多时有些buffer永远用不到的情况,最后我们选择了5个buffer进行实验。
5.3. 读者优先的读者-写者问题
核心代码:
46:#define N 1
47:#define MAXNLOOP 30
48:int readercount =0;
49:HANDLE WINAPI rmutex = CreateMutex(NULL, FALSE, "MutexToProtectCriticalResource");
50:HANDLE WINAPI wmutex = CreateMutex(NULL, FALSE, "MutexToProtectCriticalResource");
51:
52:string buffer[N];
53:int nloop = 0;
54:int in = 0;
55:int out = 0;
56:DWORD WINAPI ThreadExecuteZZWWriter(LPVOID lpParameter){
57: int *pID = (int*)lpParameter;
58: string nextp = to_string(*pID) + " wrote";
59:
60: while(1){
61: WaitForSingleObject(wmutex, INFINITE);
62: if(nloop >= MAXNLOOP) break;
63: buffer[in] = nextp;
64: in = (in + 1) % N;
65: cout << "nloop " << nloop << " writer " << *pID << " has wrote on the buffer " << in << "\n";
66: ++nloop;
67: ReleaseMutex(wmutex);
68: }
69: return 0;
70:}
71:
72:DWORD WINAPI ThreadExecuteZZWReader(LPVOID lpParameter){
73: int *pID = (int*)lpParameter;
74: string nextp;
75: while(1){
76: WaitForSingleObject(rmutex, INFINITE);
77: if(readercount == 0) WaitForSingleObject(wmutex, INFINITE);
78: ++readercount;
79: if(nloop>=MAXNLOOP) break;
80: ++nloop;
81: ReleaseMutex(rmutex);
82:
83: nextp = buffer[out];
84: out = (out +1) % N;
85: cout << "nloop " << nloop << " reader "<< *pID << " has read on the buffer " << out << " the content is writer "<< nextp << "\n";
86:
87: WaitForSingleObject(rmutex, INFINITE);
88: --readercount;
89: if(readercount==0) ReleaseMutex(wmutex);
90: ReleaseMutex(rmutex);
91: };
92: return 0;
93:}
结果输出:
结果分析:
这里我们可以直接设置buffer为1,也就是文件读写只有一个文件,方便验证效果。注意变红区域,一开始写者尚未写读者直接读出,所以content没有信息。后边写者两个进程都往其中写入了内容,当然是后者覆盖了前者,所以读者两个线程读出了writer0 的内容,这里表面读者可以并行读出。并且,只要还有读者在读,写者就不能写。实验预期目标达成。
5.4. 写者优先的读者-写者问题
94:#define N 1
95:#define MAXNLOOP 30
96:
97:int readercount =0;
98:int writercount =0;
99:
100:// mutex for make sure writer first, wmutex1 lock for writercount, wmutex2 to mutex other writers,
101:// rmutex lock for readercount
102:
103:HANDLE WINAPI mutex = CreateMutex(NULL, FALSE, "MutexToProtectCriticalResource");
104:HANDLE WINAPI wmutex1 = CreateMutex(NULL, FALSE, "MutexToProtectCriticalResource");
105:HANDLE WINAPI wmutex2 = CreateMutex(NULL, FALSE, "MutexToProtectCriticalResource");
106:HANDLE WINAPI rmutex = CreateMutex(NULL, FALSE, "MutexToProtectCriticalResource");
107:
108:string buffer[N];
109:int nloop = 0;
110:int in = 0;
111:int out = 0;
112:
113:DWORD WINAPI ThreadExecuteZZWWriter(LPVOID lpParameter){
114: int *pID = (int*)lpParameter;
115: string nextp = to_string(*pID) + " wrote";
116:
117: while(1){
118: WaitForSingleObject(wmutex1, INFINITE);
119: if(nloop >= MAXNLOOP) break;
120: if(writercount==0) WaitForSingleObject(mutex, INFINITE);
121: ++writercount;
122: ReleaseMutex(wmutex1);
123:
124: WaitForSingleObject(wmutex2, INFINITE);
125: buffer[in] = nextp;
126: in = (in + 1) % N;
127: ++nloop;
128: cout << "nloop " << nloop << " writer " << *pID << " has wrote on the buffer " << in << "\n";
129:
130: ReleaseMutex(wmutex2);
131:
132: WaitForSingleObject(wmutex1, INFINITE);
133: --writercount;
134: if(writercount==0) ReleaseMutex(mutex);
135: ReleaseMutex(wmutex1);
136: }
137: return 0;
138:}
139:
140:DWORD WINAPI ThreadExecuteZZWReader(LPVOID lpParameter){
141: int *pID = (int*)lpParameter;
142: string nextp;
143: while(1){
144: WaitForSingleObject(mutex, INFINITE);
145: WaitForSingleObject(rmutex, INFINITE);
146: if(readercount == 0) WaitForSingleObject(wmutex2, INFINITE);
147: ++readercount;
148: if(nloop>=MAXNLOOP) break;
149:
150: ReleaseMutex(rmutex);
151: ReleaseMutex(mutex);
152:
153: nextp = buffer[out];
154: out = (out +1) % N;
155: ++nloop;
156: cout << "nloop " << nloop << " reader "<< *pID << " has read on the buffer " << out << " the content is writer "<< nextp << "\n";
157:
158: WaitForSingleObject(rmutex, INFINITE);
159: --readercount;
160: if(readercount==0) ReleaseMutex(wmutex2);
161: ReleaseMutex(rmutex);
162: };
163: return 0;
164:}
理想的结果是,writercount大于0,即有写者在排队,这时,读者是被阻塞的,但是这样的结果目前没有很好的方法可视化,只能简单从理论上的代码上进行判断,实验成功。
5.5. 哲学家就餐问题
核心代码:
165:#define MAXNLOOP 30
166:int nloop = 0;
167:HANDLE WINAPI chopstick0 = CreateMutex(NULL, FALSE, "0");
168:HANDLE WINAPI chopstick1 = CreateMutex(NULL, FALSE, "1");
169:HANDLE WINAPI chopstick2 = CreateMutex(NULL, FALSE, "2");
170:HANDLE WINAPI chopstick3 = CreateMutex(NULL, FALSE, "3");
171:HANDLE WINAPI chopstick4 = CreateMutex(NULL, FALSE, "4");
172:HANDLE WINAPI chopstick[5];
173:HANDLE WINAPI mutex = CreateMutex(NULL, FALSE, "mutex");
174:DWORD WINAPI ThreadExecuteZZWPhilosopher(LPVOID lpParameter){
175: int *pID = (int *)lpParameter;
176:
177: while(1){
178:// WaitForSingleObject(mutex, INFINITE);
179: cout << "Philosopher " << *pID << " is thinking" << endl;
180:// ReleaseMutex(mutex);
181:
182: WaitForSingleObject(mutex, INFINITE);
183:// WaitForSingleObject(semCount, INFINITE);
184: WaitForSingleObject(chopstick[*pID], INFINITE);
185: WaitForSingleObject(chopstick[((*pID)+1)%5], INFINITE);
186: WaitForSingleObject(mutex, INFINITE);
187:
188: if(nloop > MAXNLOOP) break;
189: cout << "nloop " << nloop << " Philosopher " << *pID << " is eating" << endl;
190: ++nloop;
191:// ReleaseMutex(mutex);
192: ReleaseMutex(chopstick[((*pID)+1)%5]);
193: ReleaseMutex(chopstick[*pID]);
194:// ReleaseSemaphore(semCount, 1, NULL);
195: }
196:}
通过实验结果截图,可以看出(输出buffer相互混杂,考虑到实验原理,这里没有采取互斥输出)5个线程并行think,之后线程3拿到筷子开始吃饭,后边1号拿到筷子开始吃饭。实验成功。
6. 实验汇总
6.1. 实验要求完成情况
成功完成实验要求。
6.2. 技术难点及解决方案
一开始在生产者-消费者部分,nloop即实验运行的次数总是会比设定的最大次数多两次。这样的结果,本人认为是判断循环停止条件不在同步机管控范围内,这样在判断时还没有超过最大循环次数,但是这时候有其他线程占用,最后运行出来就是超过了最大循环次数的。我们可以把停止条件位置修改一下,这样就可以在同步管控代码区进行处理,保证循环次数的控制。
又回想到实验5中,也是像原来那样在整个循环块进行条件判断,但是却不会超过循环次数,这里考虑是之前的银行转账问题中只有两个线程,而现在有四个线程。
在写者优先的读者写者问题中,发现可视化写者优先的问题十分难实现,最多只能从代码上判断实验的正确与否,确实比较遗憾。并且,本人发现,读写的顺序和子线程在主线程中创建的顺序基本一致,因为创建的过程是顺序的,并且每个子线程执行的代码又是一样的,所以有这样的结果。这样又降低了实验的可靠性。总之在多线程程序中,代码的的正确性还需要更多的方法去验证。
在哲学家问题中,采用了for循环来创建线程,代码是:
For(int i =0; i<5;++i){
hThread [0] = CreateThread(NULL, 0, ThreadExecuteZZWPhilosopher, &i, 0, NULL
}
结果最后的输出结果,只有线程5在跑,后来发现是i的问题。所有线程在运行时,i的值只在一个地址上自增,最后也是把指针传入线程函数,这样最后i全变成了5,所以有这样的结果。
而且最后发现,所有eating的线程最后都会变成一个,换句话说,一开始,还会出现各个线程之间相互抢筷子,但是最后基本就是只有一个线程在拿筷子,放下再拿。这里认为可能是多核处理器自动把部分线程直接阻塞了,或者是会自动保持有一个线程优先级最高,这样就只有它拿的到筷子。这里还需要深入的学习。
6.3. 实验感想和经验总结
通过实验六的典型同步案例,对线程/进程间的同步有了更深的理解,特别是几个关键的函数。但是,通过最后的交互输出结果也发现,可能线程之间配合在多核系统中还有许多更加复杂的问题需要学习。
6.4. 参考链接
- ZGSOS操作系统实验指导《实验课题6_典型同步问题模拟处理编程设计与实现》