背景知识
Windows 2000提供的常用对象可分成三类:核心应用服务、线程同步和线程间通讯。其中,开发人员可以使用线程同步对象来协调线程和进程的工作,以使其共享信息并执行任务。此类对象包括互锁数据、临界段、事件、互斥体和信号等。
多线程编程中关键的一步是保护所有的共享资源,工具主要有互锁函数、临界段和互斥体等;另一个实质性部分是协调线程使其完成应用程序的任务,为此,可利用内核中的事件对象和信号。
在进程内或进程间实现线程同步的最方便的方法是使用事件对象,这一组内核对象允许一个线程对其受信状态进行直接控制 (见表3-1) 。
而互斥体则是另一个可命名且安全的内核对象,其主要目的是引导对共享资源的访问。拥有单一访问资源的线程创建互斥体,所有想要访问该资源的线程应该在实际执行操作之前获得互斥体,而在访问结束时立即释放互斥体,以允许下一个等待线程获得互斥体,然后接着进行下去。
与事件对象类似,互斥体容易创建、打开、使用并清除。利用CreateMutex() API可创建互斥体,创建时还可以指定一个初始的拥有权标志,通过使用这个标志,只有当线程完成了资源的所有的初始化工作时,才允许创建线程释放互斥体。
表3-1 用于管理事件对象的API
API名称 | 描述 |
CreateEvent() | 在内核中创建一个新的事件对象。此函数允许有安全性设置、手工还是自动重置的标志以及初始时已接受还是未接受信号状态的标志 |
OpenEvent() | 创建对已经存在的事件对象的引用。此API函数需要名称、继承标志和所需的访问级别 |
SetEvent() | 将手工重置事件转化为已接受信号状态 |
ResetEvent() | 将手工重置事件转化为非接受信号状态 |
PulseEvent() | 将自动重置事件对象转化为已接受信号状态。当系统释放所有的等待它的线程时此种转化立即发生 |
为了获得互斥体,首先,想要访问调用的线程可使用OpenMutex() API来获得指向对象的句柄;然后,线程将这个句柄提供给一个等待函数。当内核将互斥体对象发送给等待线程时,就表明该线程获得了互斥体的拥有权。当线程获得拥有权时,线程控制了对共享资源的访问——必须设法尽快地放弃互斥体。放弃共享资源时需要在该对象上调用ReleaseMute() API。然后系统负责将互斥体拥有权传递给下一个等待着的线程 (由到达时间决定顺序) 。
1、实验目的
1) 回顾系统进程、线程的有关概念,加深对Windows 2000线程的理解。
2) 了解互斥体对象,通过对生产者消费者等进程间同步与互斥经典算法的实现,加深对P、V原语以及利用P、V原语进行进程间同步与互斥操作的理解。
(1). 生产者消费者问题
步骤1:创建一个“Win32 Consol Application”工程,然后拷贝清单3-1中的程序,编译成可执行文件。
步骤2:在“命令提示符”窗口运行步骤1中生成的可执行文件。运行结果:
范例:E:\课程\os课\os实验\程序\os11\debug>os31
(假设编译生成的可执行文件是os31.exe)
图 1运行截图一
图 2运行截图二
步骤3:仔细阅读源程序,找出创建线程的WINDOWS API函数,回答下列问题:线程的第一个执行函数是什么(从哪里开始执行)?它位于创建线程的API函数的第几个参数中?
DWORD WINAPI Producer(LPVOID lpPara) 第三个参数
hThreads[i]=CreateThread(NULL,0,Producer,NULL,0,&producerID[i]);_
步骤4:修改清单3-1中的程序,调整生产者线程和消费者线程的个数,使得消费者数目大与生产者,看看结果有何不同。运行结果:
const unsigned short PRODUCERS_COUNT = 3; //生产者的个数
const unsigned short CONSUMERS_COUNT = 6; //消费者的个数
图 3调整生产者线程和消费者线程的个数
图 4运行结果截图
图 5运行结果截图
从中你可以得出什么结论:
当消费者数目大于生产者时,相对于生产者生产的速率,消费者线程能够更快速地消费产品,因此消费者的等待时间会减少,另外,当消费者线程数量多于生产者时,队列中的产品能够更快速地被消费,从而避免了队列的过度堆积和浪费。这可以提高队列的利用率,使其更有效地作为生产者和消费者之间的缓冲区。但是,如果生产者线程的生产速率无法跟上消费者线程的消费速率,生产者线程可能会在生成产品之前被阻塞,等待队列中的空间变得可用。这可能导致生产者的效率降低。
步骤5:修改清单3-1中的程序,按程序注释中的说明修改信号量EmptySemaphore的初始化方法,看看结果有何不同。运行结果:
EmptySemaphore=CreateSemaphore(NULL,SIZE_OF_BUFFER,SIZE_OF_BUFFER,NULL);_改为
EmptySemaphore=CreateSemaphore(NULL,0,SIZE_OF_BUFFER-1,NULL);_
图 6修改信号量
图 7运行结果截图
图 8运行结果截图
由图7和图8可知修改信号以后无法执行
步骤6:根据步骤4的结果,并查看MSDN,回答下列问题
1)CreateMutex中有几个参数,各代表什么含义。
_CreateMutex中有三个参数,各代表的含义如下:
第一个参数是安全属性指针,通常设置为NULL,表示使用默认的安全属性。
第二个参数如果设置为TRUE,则互斥对象将被初始化为有信号状态(即可以被获取),如果设置为FALSE,则互斥对象将被初始化为无信号状态(即不可被获取)。这里设置为FALSE,表示在创建时互斥对象不可用。
第三个参数是互斥对象的名称,如果设置为NULL,则互斥对象是匿名的。
2)CreateSemaphore中有几个参数,各代表什么含义,信号量的初值在第几个参数中。
CreateSemaphore中有四个参数,各代表的含义如下:
第一个参数是安全属性指针,通常设置为NULL,表示使用默认的安全属性。
第二个参数是信号量的初始值,即信号量对象创建时的值。
第三个参数是信号量的最大值,即信号量对象可以达到的最大值。
第四个参数是信号量的名称,如果设置为NULL,则信号量是匿名的。
3)程序中P、V原语所对应的实际Windows API函数是什么,写出这几条语句。
WaitForSingleObject(EmptySemaphore,INFINITE); P操作
WaitForSingleObject(Mutex,INFINITE);
ReleaseMutex(Mutex); V操作
ReleaseSemaphore(FullSemaphore,1,NULL);
4)CreateMutex能用CreateSemaphore替代吗?尝试修改程序3-1,将信号量Mutex完全用CreateSemaphore及相关函数实现。写出要修改的语句:
CreateMutex通常不用CreateSemaphore替代,因为它们是用于不同目的的同步原语。然而,理论上可以使用CreateSemaphore来模拟互斥量(mutex)的行为,但这样做并不常见,也不直观。
将信号量Mutex完全用CreateSemaphore及相关函数实现,需要更改的部分如下:
首先,更改Mutex的创建语句:
// Mutex = CreateMutex(NULL, FALSE, NULL); // 原始互斥量创建
Mutex = CreateSemaphore(NULL, 1, 1, NULL); // 使用信号量模拟互斥量
然后,在Producer和Consumer线程中,用信号量替换互斥量的操作:
生产者线程中:
// WaitForSingleObject(Mutex, INFINITE); // 原始互斥量获取
WaitForSingleObject(Mutex, INFINITE); // 使用信号量模拟互斥量的获取
// ... 生产操作 ...
ReleaseSemaphore(Mutex, 1, NULL); // 释放信号量,模拟互斥量的释放
消费者线程中同样进行替换。
注意,由于我们使用信号量来模拟互斥量,所以信号量的初始值和最大值都被设置为1,这样信号量的行为就与互斥量类似了。但是,这样做可能会让代码更难以理解和维护,因为信号量和互斥量在Windows API中有明确的区分。
(2). 读者写者问题
根据实验(1)中所熟悉的P、V原语对应的实际Windows API函数,并参考教材中读者、写者问题的算法原理,尝试利用Windows API函数实现第一类读者写者问题(读者优先)。
具体代码如下:
#include <windows.h>
#include <stdio.h>
// 信号量用于控制同时访问的读者数量
HANDLE hSemReaders = NULL;
// 事件用于同步写者和读者
HANDLE hEventWriter = NULL;
// 计数器用于跟踪当前正在访问的读者数量
LONG lReaderCount = 0;
// 互斥量用于保护读者计数器的访问
HANDLE hMutex = NULL;
// 初始化同步对象
void Initialize() {
hSemReaders = CreateSemaphore(NULL, 1, LONG_MAX, NULL); // 初始时允许一个读者或没有读者
hEventWriter = CreateEvent(NULL, TRUE, TRUE, NULL); // 写者事件初始时置为有信号状态(允许写者)
hMutex = CreateMutex(NULL, FALSE, NULL); // 互斥量初始时不被任何线程拥有
if (!hSemReaders || !hEventWriter || !hMutex) {
// 处理错误
// ...
}
}
// 读者访问共享资源
void Reader(void) {
WaitForSingleObject(hMutex, INFINITE); // 等待互斥量,保护读者计数器
if (++lReaderCount == 1) { // 第一个读者到来时,重置写者事件
ResetEvent(hEventWriter);
}
ReleaseMutex(hMutex); // 释放互斥量
WaitForSingleObject(hSemReaders, INFINITE); // 等待信号量,允许读者进入
// 访问共享资源...
ReleaseSemaphore(hSemReaders, 1, NULL); // 访问完成后释放信号量
WaitForSingleObject(hMutex, INFINITE); // 再次等待互斥量,保护读者计数器
if (--lReaderCount == 0) { // 最后一个读者离开时,设置写者事件为有信号状态
SetEvent(hEventWriter);
}
ReleaseMutex(hMutex); // 释放互斥量
}
// 写者访问共享资源
void Writer(void) {
WaitForSingleObject(hEventWriter, INFINITE); // 等待写者事件,确保没有读者且写者可以进入
// 访问共享资源(写操作)...
// 写操作完成后,重置写者事件,允许新的读者或写者进入
ResetEvent(hEventWriter);
}
// 清理资源
void Cleanup() {
CloseHandle(hSemReaders);
CloseHandle(hEventWriter);
CloseHandle(hMutex);
}
int main() {
Initialize();
// 模拟读者和写者访问共享资源
Cleanup();
return 0;
}
通过本次实验,我深入理解了进程与线程的概念、互斥体对象的作用以及P、V原语在进程间同步与互斥操作中的应用。同时,通过实现生产者消费者问题,我加深了对同步与互斥机制的理解,并掌握了在Windows 2000操作系统中使用相关API函数进行编程的方法。
清单3-1 生产者消费者问题
#include <windows.h>
#include <iostream>
const unsigned short SIZE_OF_BUFFER = 2; //缓冲区长度
unsigned short ProductID = 0; //产品号
unsigned short ConsumeID = 0; //将被消耗的产品号
unsigned short in = 0; //产品进缓冲区时的缓冲区下标
unsigned short out = 0; //产品出缓冲区时的缓冲区下标
int buffer[SIZE_OF_BUFFER]; //缓冲区是个循环队列
bool p_ccontinue = true; //控制程序结束
HANDLE Mutex; //用于线程间的互斥
HANDLE FullSemaphore; //当缓冲区满时迫使生产者等待
HANDLE EmptySemaphore; //当缓冲区空时迫使消费者等待
DWORD WINAPI Producer(LPVOID); //生产者线程
DWORD WINAPI Consumer(LPVOID); //消费者线程
int main()
{
//创建各个互斥信号
//注意,互斥信号量和同步信号量的定义方法不同,互斥信号量调用的是CreateMutex函数,同步信号量
//调用的是CreateSemaphore函数,函数的返回值都是句柄。
Mutex = CreateMutex(NULL,FALSE,NULL);
EmptySemaphore = CreateSemaphore(NULL,SIZE_OF_BUFFER,SIZE_OF_BUFFER,NULL);
//将上句做如下修改,看看结果会怎样
//EmptySemaphore = CreateSemaphore(NULL,0,SIZE_OF_BUFFER-1,NULL);
FullSemaphore = CreateSemaphore(NULL,0,SIZE_OF_BUFFER,NULL);
//调整下面的数值,可以发现,当生产者个数多于消费者个数时,
//生产速度快,生产者经常等待消费者;反之,消费者经常等待
const unsigned short PRODUCERS_COUNT = 3; //生产者的个数
const unsigned short CONSUMERS_COUNT = 1; //消费者的个数
//总的线程数
const unsigned short THREADS_COUNT = PRODUCERS_COUNT+CONSUMERS_COUNT;
HANDLE hThreads[THREADS_COUNT]; //各线程的handle
DWORD producerID[PRODUCERS_COUNT]; //生产者线程的标识符
DWORD consumerID[CONSUMERS_COUNT]; //消费者线程的标识符
//创建生产者线程
for (int i=0;i<PRODUCERS_COUNT;++i){
hThreads[i]=CreateThread(NULL,0,Producer,NULL,0,&producerID[i]);
if (hThreads[i]==NULL) return -1;
}
//创建消费者线程
for (int i=0;i<CONSUMERS_COUNT;++i){
hThreads[PRODUCERS_COUNT+i]=CreateThread(NULL,0,Consumer,NULL,0,&consumerID[i]);
if (hThreads[i]==NULL) return -1;
}
while(p_ccontinue){
if(getchar()){ //按回车后终止程序运行
p_ccontinue = false;
}
}
return 0;
}
//生产一个产品。简单模拟了一下,仅输出新产品的ID号
void Produce()
{
std::cout << std::endl<< "Producing " << ++ProductID << " ... ";
std::cout << "Succeed" << std::endl;
}
//把新生产的产品放入缓冲区
void Append()
{
std::cerr << "Appending a product ... ";
buffer[in] = ProductID;
in = (in+1)%SIZE_OF_BUFFER;
std::cerr << "Succeed" << std::endl;
//输出缓冲区当前的状态
for (int i=0;i<SIZE_OF_BUFFER;++i){
std::cout << i <<": " << buffer[i];
if (i==in) std::cout << " <-- 生产";
if (i==out) std::cout << " <-- 消费";
std::cout << std::endl;
}
}
//从缓冲区中取出一个产品
void Take()
{
std::cerr << "Taking a product ... ";
ConsumeID = buffer[out];
buffer[out] = 0;
out = (out+1)%SIZE_OF_BUFFER;
std::cerr << "Succeed" << std::endl;
//输出缓冲区当前的状态
for (int i=0;i<SIZE_OF_BUFFER;++i){
std::cout << i <<": " << buffer[i];
if (i==in) std::cout << " <-- 生产";
if (i==out) std::cout << " <-- 消费";
std::cout << std::endl;
}
}
//消耗一个产品
void Consume()
{
std::cout << "Consuming " << ConsumeID << " ... ";
std::cout << "Succeed" << std::endl;
}
//生产者
DWORD WINAPI Producer(LPVOID lpPara)
{
while(p_ccontinue){
WaitForSingleObject(EmptySemaphore,INFINITE); //p(empty);
WaitForSingleObject(Mutex,INFINITE); //p(mutex);
Produce();
Append();
Sleep(1500);
ReleaseMutex(Mutex); //V(mutex);
ReleaseSemaphore(FullSemaphore,1,NULL); //V(full);
}
return 0;
}
//消费者
DWORD WINAPI Consumer(LPVOID lpPara)
{
while(p_ccontinue){
WaitForSingleObject(FullSemaphore,INFINITE); //P(full);
WaitForSingleObject(Mutex,INFINITE); //P(mutex);
Take();
Consume();
Sleep(1500);
ReleaseMutex(Mutex); //V(mutex);
ReleaseSemaphore(EmptySemaphore,1,NULL); //V(empty);
}
return 0;
}