实验三 进程同步实验

更新时间:2023-10-22 13:08:01 阅读量: 综合文库 文档下载

说明:文章内容仅供预览,部分内容可能不全。下载后的文档,内容与下面显示的完全一致。下载之前请确认下面内容是否您想要的,是否完整无缺。

实验三 进程同步机制

一、实验内容:

学习Windows有关进程/线程同步的背景知识和API,学习windows平台下常用的同步方式,并分析2个实验程序(利用信号量实现两个进程间的同步和利用互斥量实现读者写者问题),观察程序的运行情况并分析执行结果。 二、实验目的:

在本实验中,通过对互斥量(Mutex)和信号量(Semaphore)对象的了解,来加深对Windows 进程、线程同步的理解。

(1) 了解互斥量和信号量对象。

(2) 通过分析实验程序,理解管理信号量对象的API。 (3) 理解在进程中如何使用信号量对象。

(4) 通过分析实验程序,理解在线程中如何使用互斥量对象。

(5) 理解父进程创建子进程的程序设计方法,理解在主线程中创建子线程的方法。 三、实验要求:

(1) 理解Windows有关进程/线程同步的背景知识和API。

(2) 按要求运行2个程序,观察程序执行的结果,并给出要求的结果分析。

(3) 参照3-2程序,写出一个实现单个生产者—消费者问题的算法,可以使用单个缓冲区,也可以使用缓冲池,生产者随机产生任意形式的数据并放入缓冲区中,消费者则以随机的时间间隔从缓冲区中取数据,随机时间请使用随机数产生。 四、并发与同步的背景知识

Windows开发人员可以使用同步对象来协调线程和进程的工作,以使其共享信息并执行任务。此类对象包括互斥量Mutex、信号量Semaphore、事件Event等。

多进程、多线程编程中关键的一步是保护所有的共享资源,工具主要有互斥量Mutex和信号量Semaphore等;另一个是协调线程使其完成应用程序的任务,为此,可利用内核中的信号量对象或事件对象。

互斥量是一个可命名且安全的内核对象,主要目的是引导对共享资源的访问。拥有单一访问资源的线程创建互斥体,所有希望访问该资源的线程应该在实际执行操作之前获得互斥体,而在访问结束时立即释放互斥体,以允许下一个等待线程获得互斥体,然后接着进行下去。

利用CreateMutex() API可创建互斥量,创建时可以指定一个初始的拥有权标志,通过使用这个标志,只有当线程完成了资源的所有的初始化工作时,才允许创建线程释放互斥体。

为了获得互斥体,首先,想要访问调用的线程可使用OpenMutex() API来获得指向对象的句柄;然后,线程将这个句柄提供给一个等待函数。当内核将互斥体对象发送给等待线程时,就表明该线程获得了互斥体的拥有权。当线程获得拥有权时,线程控制了对共享资源的访问——必须设法尽快地放弃互斥体。放弃共享资源时需要在该对象上调用ReleaseMutex() API。然后系统负责将互斥量拥有权传递给下一个等待着的线程 (由到达时间决定顺序) 。

信号量Semaphore与互斥量Mutex的用法不同,互斥量Mutex保证任意时刻只能有一个进程或线程获得互斥体,信号量允许多个线程同时使用共享资源,这与操作系统中的Wait/Signal操作【也称PV操作】相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数或者0。每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完

共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。

此外,windows还提供了另外一个容易误导大家理解的同步对象,取名为Critical Section,中文取名为临界区,请大家注意与课本讲的临界资源、临界区的概念相区分。课本上临界区指“对临界资源进行访问的代码”;而这种称为“Critical Section”互斥机制,并不是这个意思,而是访问临界区之前的一种加锁机制,与互斥量Mutex的作用类似,只是“Critical Section”互斥机制只能在同一进程内部各个线程间使用,而Mutex互斥机制是可以跨进程使用的。 五、实验步骤

1. 信号量Semaphore对象

清单3-1程序展示如何在进程间使用信号量对象。

父进程启动时,利用CreateSemaphore () API创建一个命名的、可共享的信号量对象,并利用CreateProcess()创建子进程,然后调用WaitForSingleObject()函数去获取信号量,但是由于信号量的初始值为0,所以父进程阻塞在此,无法成功占有信号量;子进程创建后,调用OpenSemaphore()打开父进程创建的信号量,然后调用ReleaseSemaphore()函数释放了1个信号量,此后,处于阻塞状态的父进程获取了子进程释放的信号量,从而解除了阻塞,继续运行至程序结束。

清单3-1 创建和打开信号量对象在进程间传送信号 // Semaphore信号量项目 # include # include # include

// 定义一个信号量的名字

static LPCTSTR g_szSemaphoreName = \

// 本方法只是创建了一个进程的副本,以子进程模式 (由命令行指定) 工作 BOOL CreateChild() { // 提取当前可执行文件的文件名 TCHAR szFilename[MAX_PATH] ;

GetModuleFileName(NULL, szFilename, MAX_PATH) ; // 格式化用于子进程的命令行,指明它是一个EXE文件和子进程 TCHAR szCmdLine[MAX_PATH] ;

sprintf(szCmdLine, \

// 子进程的启动信息结构 STARTUPINFO si;

ZeroMemory(reinterpret_cast(&si), sizeof(si)) ; si.cb = sizeof(si); // 必须是本结构的大小

// 返回的子进程的进程信息结构 PROCESS_INFORMATION pi;

// 使用同一可执行文件和告诉它是一个子进程的命令行创建进程 BOOL bCreateOK = CreateProcess( szFilename, // 生成的可执行文件名 szCmdLine, // 指示其行为与子进程一样的标志 NULL, // 子进程句柄的安全性 NULL, // 子线程句柄的安全性

FALSE, // 不继承句柄 CREATE_NEW_CONSOLE, // 特殊的创建标志 NULL, // 新环境 NULL, // 当前目录 &si, // 启动信息结构 &pi ) ; // 返回的进程信息结构

// 释放对子进程的引用 if (bCreateOK) {

CloseHandle(pi.hProcess); CloseHandle(pi.hThread); } return(bCreateOK) ; }

// 下面的方法创建一个信号量和一个子进程,然后等待子进程在返回前释放一个信号 void WaitForChild() { HANDLE hSemaphore = CreateSemaphore( //创建一个信号量,初始值0,最大值为1 NULL, // 缺省的安全性,子进程将具有访问权限 0, //初始值0 1, //最大值为1 g_szSemaphoreName //信号量名称 );

if (hSemaphore != NULL) { cout << \信号量对象已经建立啦。\ endl; // 创建子进程 if ( CreateChild()) { cout << \父进程建立了一个子进程\ endl; // 等待,直到子进程发出信号 cout << \因为当前信号量的值为0,所以父进程暂时阻塞在此啦.\ WaitForSingleObject(hSemaphore, INFINITE); cout << \因为子进程释放了1个信号量,所以,父进程可以解除阻塞啦。\ }

// 清除句柄

CloseHandle(hSemaphore);

hSemaphore=INVALID_HANDLE_VALUE; } }

// 以下方法在子进程模式下被调用,其功能只是释放1个信号量 void SignalParent() {

// 尝试打开句柄

cout << \ endl; HANDLE hSemaphore = OpenSemaphore( SEMAPHORE_ALL_ACCESS, // 所要求的最小访问权限 FALSE, // 不是可继承的句柄 g_szSemaphoreName); // 信号量名称

if(hSemaphore != NULL) { cout<<\如果你同意释放一个信号量,请按任意键\ getchar();

cout << \此刻,子进程释放了1个信号量.\ ReleaseSemaphore(hSemaphore,1,NULL); }

// 清除句柄

CloseHandle(hSemaphore) ;

hSemaphore = INVALID_HANDLE_VALUE; }

int main(int argc, char* argv[] ) {

// 检查父进程或是子进程是否启动

if (argc>1 && strcmp(argv[1] , \ { // 向父进程发出信号 SignalParent() ;

cout << \子进程马上就要运行结束了。请按任意键继续。\ } else { // 创建一个信号量并等待子进程 WaitForChild();

cout << \父进程马上就要运行结束了。请按任意键继续。\ } getchar(); return 0; }

步骤1:编译并执行3-1.exe程序。

程序运行结果是 (分行书写) :

① __________________________________________________________________ ② __________________________________________________________________ ③ __________________________________________________________________ ④ __________________________________________________________________ ⑤ __________________________________________________________________ ⑥ __________________________________________________________________

阅读和分析程序3-1,请回答:

(1) 程序中,创建一个信号量使用了哪一个系统函数?创建时设置的信号量初始值是多少,最大值是多少?

a. __________________________________________________________________ b. __________________________________________________________________ (2) 创建一个进程 (子进程) 使用了哪一个系统函数?

____________________________________________________________________

(3) 从步骤1的输出结果,对照分析3-1程序,能够看出程序运行的流程吗?请简单描述:

________________________________________________________________________ ________________________________________________________________________ ________________________________________________________________________ ________________________________________________________________________ ________________________________________________________________________ ________________________________________________________________________

步骤2:编译程序生成执行文件3-1.exe,在命令行状态下执行程序,分别使用格式:

(1) 3-1 child

(2) 3-1 或3-1 ***

运行程序,记录执行的结果,并分行说明产生不同结果的原因。 2. 互斥量Mutex对象

注意:多线程编程需要在Visual C++中设定多线程C运行时库。打开你的工程项目后,在Visual C++的“Project”菜单下面,选择“Settings”菜单,如下两图所示,可以分别设定Debug版本和Release版本所使用的多线程C运行时库,以Debug开头的都是为Debug版本准备的,都选Multithread版本。

如果使用标准 C 库而调用VC运行时库函数,则在程序的link阶段会提示如下错误: error LNK2001: unresolved external symbol __endthreadex error LNK2001: unresolved external symbol __beginthreadex

时,该线程为进程的最后一个线程,则该线程的进程也被终止。

线程对象的状态变为发信号状态,以释放所有正在等待该线程终止的其他线程。线程的终止状态从STILL_ACTIVATE变为dwExitCode参数的值。

线程结束时不必从操作系统中移去该线程对象。当线程的最后一个句柄关闭时,该线程对象被删除。 3._beginthreadex

函数功能:如果使用C/C++语言编写多线程应用程序,一定不能使用操作系统提供的CreateThread API,而应该使用C/C++运行时库中的_beginthreadex 函数原型:

uintptr_t _beginthreadex(

void *security, //Pointer to a SECURITY_ATTRIBUTES structure unsigned stack_size,

unsigned ( __stdcall *start_address )( void * ), void *arglist,

unsigned initflag,//Initial state of new thread (0 for running or CREATE_SUSPENDED for suspended); unsigned *thrdaddr );

参数:

? security:安全属性的指针,可为NULL

? stack_size: 线程栈的大小,0代表系统默认大小

? start_address: 线程函数的运行地址,该线程执行此函数(注:函数名也是该函数

的运行地址)

? arglist:参数值指针

? initflag:线程初始状态的值,0代表就绪状态,CREATE_SUSPENDED代表挂起状

? thrdaddr:存放线程ID的变量地址 返回值:

若函数调用成功,返回值为新线程的句柄;若函数调用失败,返回值为NULL。 说明:

_beginthreadex函数与Win32 API 中的CreateThread函数类似,但有如下差异: _beginthreadex 函数初始化某些 C 运行时库变量,在线程中若需要使用 C 运行时库,则需要使用_beginthreadex 函数创建线程。 4.Sleep

函数功能:该函数对于指定的时间间隔挂起当前的执行线程。 函数原型:VOID Sleep(DWORD dwMilliseconds)

参数:dwMilliseconds:定义挂起执行线程的时间,以毫秒(ms)为单位。取值为0时,该线

程将余下的时间片交给处于就绪状态的同一优先级的其他线程。若没有处于就绪状态的同一优先级的其他线程,则函数立即返回,该线程继续执行。若取值为INFINITE则造成无限延迟。

返回值:该函数没有返回值。

说明:一个线程可以在调用该函数时将睡眠时间设为0ms,以将剩余的时间片交出。 5.CreateMutex

函数功能:该函数创建有名或者无名的互斥对象。

函数原型:HANDLE CreateMutex (LPSECURITY_ATTRIBUTES lpMutexAttributes,

BOOL bInitialOwner, LPCTSTR lpName);

参数:

? lpMutexAttributes:指向SECURITY_ATTRIBUTES结构的指针,该结构决定子进程是否

能继承返回句柄。如果lpMutexAttributes为NULL,那么句柄不能被继承。

在Windows NT中该结构的lpSecurityDescriptor成员指定新互斥对象的安全描述符。如果lpMutexAttributes为NULL,那么互斥对象获得默认的安全描述符。

? bInitialOwner:指定互斥对象的初始所属身份。如果该值为TRUE,并且调用者创建互

斥对象,那么调用线程获得互斥对象。否则,调用线程不能获得互斥对象。判断调用者是否创建互斥对象请参阅返回值部分。

? lpName:指向以NULL结尾的字符串,该字符串指定了互斥对象名。该名字的长度小于

MAX_ PATH且可以包含除反斜线(\)路径分隔符以外的任何字符。名字是区分大小写的。 如果lpName与已存在的有名互斥对象名相匹配,那么该函数要求用MUTEX_ALL_ACCESS权限访问已存在的对象。在这种情况下,由于参数bInitialOwner已被创建进程所设置,该参数被忽略。如果参数lpMutexAttributes不为NULL,它决定句柄是否解除继承,但是其安全描述符成员被忽略。

如果lpName为NULL,那么创建的互斥对象无名。

如果lpName与已存在的事件、信号量、可等待定时器、作业或者文件映射对象的名字相匹配,那么函数调用失败,并且GetLastError函数返回ERROR_INVALID_HANDLE,其原因是这些对象共享相同的名字空间。 返回值:

如果函数调用成功,返回值是互斥对象句柄;如果函数调用之前,有名互斥对象已存在,那么函数给已存在的对象返回一个句柄,并且函数GetLastError返回ERROR_ALREADY_EXISTS,否则,调用者创建互斥对象。

如果函数调用失败,则返回值为NULL。若想获得更多错误信息,请调用GetLastError函数。

说明:

由函数CreateMutex返回的句柄有MUTEX_ALL_ACCESS权限可以去访问新的互斥对象,并且可用在请求互斥对象句柄的任何函数中。

当一个互斥对象不被任何线程拥有时,处于信号态。创建该对象的线程可以使用bInitialOwner标志来请求立即获得对该互斥对象的所有权。否则,线程必须使用等待函数【例如WaitForSingleObject函数】来请求所有权。当互斥对象处于信号态,等待的线程获得对该对象的所有权时,此互斥对象的状态被设置为非信号态,等待函数返回。任意时刻,仅有一个线程能拥有该互斥对象,线程可以使用ReleaseMutex函数来释放对这个互斥对象的所有权。

若线程已经拥有了一个互斥对象,那么它可以重复调用等待函数而不会发生阻塞,一般情况下,用户不会重复等待同一个互斥对象,这种机制防止了线程因等待它已经拥有的互斥对象而发生死锁。然而,线程必须为每一次等待调用一次ReleaseMutex函数来释放该互斥对象。

两个或多个进程可以调用CreateMutex来创建同名的互斥对象,第一个进程实际创建互斥对象,以后的进程打开已存在的互斥对象的句柄。这使得多个进程可以得到同一个互斥对象的句柄,从而减轻了用户的负担,使用户不必判断创建进程是否为第一个启动的进程。使用这种技术时,应该把bInitialOwner标志设为FALSE;否则很难确定开始时哪一个进程拥有该互斥对象。

由于多进程能够拥有相同互斥对象的句柄,通过使用这个对象,可使多进程同步。以下为共享对象机制:

? 如果CreateMutex中的lpMutexAttributes参数允许继承,由CreateProcess函数创建的子进程可以继承父进程的互斥对象句柄。

? 一个进程可以在调用DuplicateHandle函数时指定互斥对象句柄来创建一个可以被其他进程使用的双重句柄。一个进程在调用OpenMutex或CreateMutex函数时能指定互斥对象名。

? 使用CloseHandle函数关闭句柄,进程结束时系统自动关闭句柄。当最后一个句柄被关闭时,互斥对象被销毁。 6.ReleaseMutex

函数功能:该函数放弃指定互斥对象的所有权。 函数原型:BOOL ReleaseMutex(HANDLE hMutex)

参数:hMutex:互斥对象句柄。为CreateMutex或OpenMutex函数的返回值。 返回值:如果函数调用成功,那么返回值是非零值;如果函数调用失败,那么返回值是零值。

若想获得更多错误信息,请调用GetLastError函数。

说明:如果调用线程不拥有互斥对象,ReleaseMutex函数失败。

一个线程通过调用等待函数拥有互斥对象。创建该互斥对象的线程也拥有互斥对象,而不需要调用等待函数。当互斥对象的所有者线程不再需要互斥对象时,它可以调用ReleaseMutex函数。

当一个线程拥有一个互斥对象后,它可以用该互斥对象多次调用等待函数而不会阻塞。这防止一个线程等待一个它已拥有的互斥对象时出现死锁。不过,为了释放所有权,该线程必须为每一个等待操作调用一次ReleaseMutex函数。 7.WaitForSingleObject

函数功能:当下列情况之一发生时该函数返回:(1)指定对象处于信号态;(2)超时 函数原型:DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds) 参数:

? hHandle:等待对象句柄。

? dwMilliseconds:指定以毫秒为单位的超时间隔。如果超时,即使对象的状态是非信号

态的并且没有完成,函数也返回。如果dwMilliseconds是0,函数测试对象的状态并立刻返回;如果dwMilliseconds是INFINITE,函数从不超时。

返回值:如果函数调用成功,返回值表明引起函数返回的事件。可能值如下:

? WAIT_ABANDONED:指定对象是互斥对象,在线程被终止前,线程没有释放互斥对象。互斥对象的所属关系被授予调用线程,并且该互斥对象被置为非信号态。 ? WAIT_OBJECT_0:指定对象的状态被置为信号态。 ? WAIT_TIMEOUT:超时,并且对象的状态为非信号态。 如果函数调用失败,返回值是WAIT_FAILED。

说明:

WaitForSingleObjects函数决定等待条件是否被满足。如果等待条件并没有被满足,调用线程进入一个高效的等待状态,当等待满足条件时占用非常少的处理器时间。

在运行前,一个等待函数修改同步对象类型的状态。修改仅发生在引起函数返回的对象身上。例如,信号的计数减1。

WaitForSingleObject函数能等待的对象包括:Change notification(改变通告);Console input(控制台输入);Event(事件);Job(作业);Mutex(互斥对象);Process(进程);Semaphore(信号量);Thread(线程);Waitabletimer(可等待定时器)。

当使用等待函数或代码直接或间接创建窗口时,一定要小心。如果一个线程创建了任何窗口,它必须处理进程消息。消息广播被发送到系统的所有窗口。一个线程用没有超时的等待函数也许会引起系统死锁。间接创建窗口的两个例子是DDE和COM CoInitialize。因此,如果用户有一个创建窗口的线程,用MsgWaitForMultipleObjects或MsgWaitForMultipleObjectsEx函数,而不要用SignalObjeectAndWait函数。 8.WaitForMultipleObjects

函数功能: WaitForMultipleObjects函数当下列条件之一满足时返回:(1)任意一个或全

部指定对象处于信号态;(2)超时间隔已过。

函数原型:DWORD WaitForMultipleObiects(DWORD nCount,CONST HANDLE *lpHandles,

BOOL fWaitAll,DWORD dwMilliSeconds);

参数:

? nCount:指定由lpHandles所指向的数组中的句柄对象数目最大对象句柄数目

MAXIMUM_WAIT_OBJECTS。

? lpHandles:指向对象句柄数组的指针。该数组可以包含不同类型对象的句柄。在

WindowsNT中,该句柄必须有SYNCHRONIZE访问权限。若想获得更多的信息,请查看 StandardAccess Rights。

? fWaitAll:指定等待类型。如果为TRUE,当lpHandles指向的数组里的全部对象为

号态时,函数返回。如果为FALSE,当由lpHandles指向的数组里的任一对象为信号态时,函数返回。对于后者,返回值指出引起函数返回的对象。

? dwMilliseconds:指定以毫秒为单位的超时间隔。如果超时,即使bWaitAll参数指

定的条件没有满足,函数也返回。如果dwMilliseconds是0,函数测试指定对象的状态并立刻返回。如果dwMilliseconds是INFINITE,函数从不超时。

返回值: 如果函数调用成功,返回值表明引起函数返回的事件。可能值如下:

? WAIT_OBJECT_0到WAIT_OBJECT_0 + nCount-1: 如果bWaitAll为TRUE,那么返回

值表明所有指定对象的状态为信号态。如果bWaitA11为FALSE,那么返回值减去 WAIT_OBJECT_0表明引起函数返回的对象的pHandles数组索引。如果多于一个对象变为信号态,则返回的是数组索引最小的信号态对象索引。 ? WAIT_ABANDONED_0到WAIT_ABANDONED_0 + nCount-1:如果bWaitAll为TRUE,那 么

返回值表明所有指定对象的状态为信号态,并且至少一个对象是己放弃的互斥对象。如果bWaitAll为FALSE,那么返回值减去WAIT_ABANDONED_0表明引起函数返回的放弃互斥对象的pHandles数组索引。

? WAIT_TIMEOUT:超时并且由参数bWaitAll指定的条件没有满足。 如果函数调用失败,返回值是WAIT_FAILED。

9.信号量包含的几个操作原语:

http://msdn.microsoft.com/en-us/library/ms685129(v=VS.85).aspx 创建一个信号量:CreateSemaphore() 打开一个信号量:OpenSemaphore() 释放信号量: ReleaseSemaphore() 等待信号量: WaitForSingleObject()

函数原型:

HANDLE CreateSemaphore (

LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, //Semaphore的安全属性,可为NULL LONG lInitialCount, //信号量的初始值 LONG lMaximumCount, //允许的信号量最大值 LPCTSTR lpName //信号量的名字 ); 返回值:

If the function succeeds, the return value is a handle to the semaphore object. If the named semaphore object existed before the function call, the function returns a handle to the existing object and GetLastError returns ERROR_ALREADY_EXISTS.

If the function fails, the return value is NULL. To get extended error information, call GetLastError.

函数原型:

HANDLE OpenSemaphore (

DWORD dwDesiredAccess, //希望获得的Semaphore的访问属性,

//例如SYNCHRONIZE或SEMAPHORE_ALL_ACCESS

BOOL bInheritHandle, //TRUE表示子进程可以继承该Semaphore的句柄; FALSE表示

子进程不可继承该Semaphore的句柄

LPCTSTR lpName //想打开的Semaphore的名字,需保证该名字所对应Semaphore

已经创建

);

函数原型:

BOOL ReleaseSemaphore (

HANDLE hSemaphore, //想释放(signal)信号的信号量句柄 LONG lReleaseCount, //想释放(signal)的信号数量

LPLONG lpPreviousCount //在释放信号前的信号值快照,可能已经被其他线程或进程修改 );

本文来源:https://www.bwwdw.com/article/cl6f.html

Top