一、实验题目
Windows 线程的互斥与同步
二、实验目的
(1) 回顾操作系统进程、线程的有关概念,加深对 Windows 线程的理解。
(2) 了解互斥体对象,利用互斥与同步操作编写生产者-消费者问题的并发程序,加深对 P (即 semWait)、V(即 semSignal)原语以及利用 P、V 原语进行进程间同步与互斥操作的理解
三、实验内容(实验原理/运用的理论知识、算法/程序流程图、步骤和方法、关键代码)
生产者消费者问题
步骤 1:创建一个“Win32 Consol Application”工程,然后拷贝清单 5-1 中的程序,编译成可执行
文件。
步骤 2:在“命令提示符”窗口运行步骤 1 中生成的可执行文件,列出运行结果。
步骤 3:仔细阅读源程序,找出创建线程的 WINDOWS API 函数,回答下列问题:线程的第一
个执行函数是什么(从哪里开始执行)?它位于创建线程的 API 函数的第几个参数中?
步骤 4:修改清单 5-1 中的程序,调整生产者线程和消费者线程的个数,使得消费者数目大与
生产者,看看结果有何不同。察看运行结果,从中你可以得出什么结论?
步骤 5:修改清单 5-1 中的程序,按程序注释中的说明修改信号量 EmptySemaphore 的初始化方
法,看看结果有何不同。
步骤 6:根据步骤 4 的结果,并查看 MSDN,回答下列问题:
1)CreateMutex 中有几个参数,各代表什么含义。
2)CreateSemaphore 中有几个参数,各代表什么含义,信号量的初值在第几个参数中。
3)程序中 P、V 原语所对应的实际 Windows API 函数是什么,写出这几条语句。
4)CreateMutex 能用 CreateSemaphore 替代吗?尝试修改程序 5-1,将信号量 Mutex 完全用
CreateSemaphore 及相关函数实现。写出要修改的语句。
问题回答:
步骤 3:
创建线程的Windows API函数是CreateThread。
线程的第一个执行函数是Producer函数(生产者线程的入口函数)。它位于CreateThread的第三个参数lpStartAddress中。
步骤 4:
调整生产者线程和消费者线程的个数,使得消费者数目大于生产者,可以观察到以下结果:
当消费者线程数目大于生产者线程数目时,消费者线程可能更频繁地等待缓冲区中有产品可消费,因此消费者线程的执行速度可能较慢。
生产者线程可能更频繁地生产产品,因为消费者线程较慢消耗产品,缓冲区中可能较少空位,导致生产者线程等待EmptySemaphore的次数增加。
步骤 5:
修改信号量EmptySemaphore的初始化方法,将第二个参数设置为0,即初始化为0个空位,代码示例:EmptySemaphore = CreateSemaphore(NULL, 0, SIZE_OF_BUFFER, NULL)。
结果将导致所有的生产者线程在开始时无法生产产品,因为EmptySemaphore信号量初始为0,生产者线程在WaitForSingleObject(EmptySemaphore, INFINITE)处被阻塞,等待消费者线程消耗产品。
步骤 6:
CreateMutex中有3个参数,各代表的含义如下:
参数1:lpMutexAttributes,指向SECURITY_ATTRIBUTES结构的指针,用于设置互斥对象的安全属性。可以为NULL,表示使用默认的安全属性。
参数2:bInitialOwner,指示是否为调用线程拥有互斥对象的初始所有权。TRUE表示拥有,FALSE表示未拥有。
参数3:lpName,互斥对象的名称。可以为NULL。
CreateSemaphore中有4个参数,各代表的含义如下:
参数1:lpSemaphoreAttributes,指向SECURITY_ATTRIBUTES结构的指针,用于设置信号量的安全属性。可以为NULL,表示使用默认的安全属性。
参数2:lInitialCount,信号量的初始计数器值,即可用的资源数量。
参数3:lMaximumCount,信号量的最大计数器值,即计数器的上限。
参数4:lpName,信号量的名称。可以为NULL。信号量的初值在第二个参数lInitialCount中。
P、V原语所对应的实际Windows API函数是:
P操作对应的是WaitForSingleObject函数,用于等待一个对象的信号状态。
V操作对应的是ReleaseSemaphore函数,用于增加信号量的计数器值。
CreateMutex可以用CreateSemaphore替代,但需要做一些修改:
将CreateMutex修改为CreateSemaphore,同时修改互斥锁的初始化和使用方法。
使用WaitForSingleObject来替代WaitForSingleObject(mutex, INFINITE),等待信号量的状态。
使用ReleaseSemaphore来替代ReleaseMutex(mutex),释放信号量。
修改代码示例:
// 初始化互斥锁
mutex = CreateSemaphore(NULL, 1, 1, NULL);
// 生产者线程
WaitForSingleObject(emptySemaphore, INFINITE);
WaitForSingleObject(mutex, INFINITE);
// ...
ReleaseSemaphore(mutex, 1, NULL);
// 消费者线程
WaitForSingleObject(fullSemaphore, INFINITE);
WaitForSingleObject(mutex, INFINITE);
// ...
ReleaseSemaphore(mutex, 1, NULL);
注意,由于CreateSemaphore初始化的互斥锁计数器初始值为1,所以这里使用了1作为CreateSemaphore的初始计数器值,并在WaitForSingleObject和ReleaseSemaphore中将第二个参数设置为1。
四、实验结果与分析
实验结果:
程序创建了多个生产者线程和消费者线程。
生产者线程负责生产产品,每次生产后将产品放入缓冲区。
消费者线程负责从缓冲区中取出产品并消耗。
生产者和消费者之间通过互斥锁(Mutex)和信号量(EmptySemaphore和FullSemaphore)实现同步和互斥。
实验分析:
在主函数中,首先创建了互斥锁(Mutex)和两个信号量(EmptySemaphore和FullSemaphore)。互斥锁用于保护共享资源的访问,信号量用于控制生产者和消费者的同步。
根据设置的生产者和消费者数量,创建了相应数量的线程,并分别执行Producer和Consumer函数。
生产者线程在每次生产产品之前,首先等待EmptySemaphore信号量,确保缓冲区有可用的位置。然后获取互斥锁Mutex,进行生产操作,将产品放入缓冲区,并释放互斥锁Mutex和FullSemaphore信号量。
消费者线程在每次消耗产品之前,首先等待FullSemaphore信号量,确保缓冲区中有可用的产品。然后获取互斥锁Mutex,进行消耗操作,从缓冲区中取出产品,并释放互斥锁Mutex和EmptySemaphore信号量。
在每个生产者和消费者的操作中,通过输出语句打印相关信息,包括生产的产品号、缓冲区的状态以及消费的产品号。
主函数中的while循环用于控制程序的运行,当按下回车键时,设置p_ccontinue为false,结束生产者和消费者的线程运行。
由于涉及多线程和同步操作,实际运行结果可能因为线程调度和竞态条件的不确定性而有所不同。以上分析是一种可能的情况。
需要注意的是,这段代码在创建信号量时有两种方式的注释,可以根据需要进行修改,观察对程序运行的影响。
总的来说,这个实验通过模拟生产者和消费者的行为,展示了线程间同步和互斥的机制。生产者和消费者通过缓冲区进行交互,确保生产者不会在缓冲区满时继续生产,消费者不会在缓冲区空时继续消耗。通过互斥锁和信号量的合理运用,保证了线程的安全和正确的执行顺序。
五、小结与心得体会
1.多线程编程:该实验涉及多线程编程,通过创建多个线程并使用线程同步机制实现线程间的协调与通信。
2.生产者-消费者模型:该实验基于生产者-消费者模型,生产者生成产品并放入缓冲区,消费者从缓冲区中取出产品进行消费。
3.缓冲区的使用:缓冲区被设计为循环队列,使用数组来表示。生产者和消费者通过维护缓冲区的读写指针(in和out)来实现对缓冲区的访问。
4.线程同步与互斥:为了避免生产者和消费者同时访问缓冲区而导致数据不一致的问题,使用互斥信号量(Mutex)进行互斥操作。同时,通过同步信号量(FullSemaphore和EmptySemaphore)实现生产者和消费者之间的同步。
5.线程间通信:生产者和消费者之间通过信号量进行通信和同步,当缓冲区满时生产者等待,当缓冲区空时消费者等待。
6.通过以上实验,可以深入理解多线程编程的概念和技术,以及生产者-消费者模型在多线程环境下的应用。这对于并发编程和系统设计有着重要的意义。