事件(Event)
事件对象也可以通过通知操作的方式来保持线程的同步。并且可以实现不同进程中的线程同步操作。
信号量包含的几个操作原语:
CreateEvent() 创建一个事件
OpenEvent() 打开一个事件
SetEvent() 回置事件
WaitForSingleObject() 等待一个事件
WaitForMultipleObjects()等待多个事件
WaitForMultipleObjects 函数原型:
1 WaitForMultipleObjects( 2 IN DWORD nCount, // 等待句柄数 3 IN CONST HANDLE *lpHandles, //指向句柄数组 4 IN BOOL bWaitAll, //是否完全等待标志 5 IN DWORD dwMilliseconds //等待时间 6 )
参数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。fWaitAll对指定的这nCount个内核对象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。 dwMilliseconds在这里的作用与在WaitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回 WAIT_TIMEOUT。在描述线程通信时曾使用过事件内核对象来进行线程间的通信,除此之外,事件内核对象也可以通过通知操作的方式来保持线程的同步。对于前面那段使用临界区保持线程同步的代码可用事件对象的线程同步方法改写如下:
1 // 事件句柄 2 HANDLE hEvent = NULL; 3 // 共享资源 4 char g_cArray[10]; 5 …… 6 UINT ThreadProc12(LPVOID pParam) 7 { 8 // 等待事件置位 9 WaitForSingleObject(hEvent, INFINITE); 10 // 对共享资源进行写入操作 11 for (int i = 0; i < 10; i++) 12 { 13 g_cArray[i] = 'a'; 14 Sleep(1); 15 } 16 // 处理完成后即将事件对象置位 17 SetEvent(hEvent); 18 return 0; 19 } 20 UINT ThreadProc13(LPVOID pParam) 21 { 22 // 等待事件置位 23 WaitForSingleObject(hEvent, INFINITE); 24 // 对共享资源进行写入操作 25 for (int i = 0; i < 10; i++) 26 { 27 g_cArray[10 - i - 1] = 'b'; 28 Sleep(1); 29 } 30 // 处理完成后即将事件对象置位 31 SetEvent(hEvent); 32 return 0; 33 } 34 …… 35 void CSample08View::OnEvent() 36 { 37 // 创建事件 38 hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 39 // 事件置位 40 SetEvent(hEvent); 41 // 启动线程 42 AfxBeginThread(ThreadProc12, NULL); 43 AfxBeginThread(ThreadProc13, NULL); 44 // 等待计算完毕 45 Sleep(300); 46 // 报告计算结果 47 CString sResult = CString(g_cArray); 48 AfxMessageBox(sResult); 49 }
在创建线程前,首先创建一个可以自动复位的事件内核对象hEvent,而线程函数则通过WaitForSingleObject()等待函数无限等待hEvent的置位,只有在事件置位时WaitForSingleObject()才会返回,被保护的代码将得以执行。对于以自动复位方式创建的事件对象,在其置位后一被WaitForSingleObject()等待到就会立即复位,也就是说在执行ThreadProc12()中的受保护代码时,事件对象已经是复位状态的,这时即使有ThreadProc13()对CPU的抢占,也会由于WaitForSingleObject()没有hEvent的置位而不能继续执行,也就没有可能破坏受保护的共享资源。在ThreadProc12()中的处理完成后可以通过SetEvent()对hEvent的置位而允许ThreadProc13()对共享资源g_cArray的处理。这里SetEvent()所起的作用可以看作是对某项特定任务完成的通知。
使用临界区只能同步同一进程中的线程,而使用事件内核对象则可以对进程外的线程进行同步,其前提是得到对此事件对象的访问权。可以通过OpenEvent()函数获取得到,其函数原型为:
1 HANDLE OpenEvent( 2 DWORD dwDesiredAccess, // 访问标志 3 BOOL bInheritHandle, // 继承标志 4 LPCTSTR lpName // 指向事件对象名的指针 5 );
如果事件对象已创建(在创建事件时需要指定事件名),函数将返回指定事件的句柄。对于那些在创建事件时没有指定事件名的事件内核对象,可以通过使用内核对象的继承性或是调用DuplicateHandle()函数来调用CreateEvent()以获得对指定事件对象的访问权。在获取到访问权后所进行的同步操作与在同一个进程中所进行的线程同步操作是一样的。
如果需要在一个线程中等待多个事件,则用WaitForMultipleObjects()来等待。WaitForMultipleObjects()与WaitForSingleObject()类似,同时监视位于句柄数组中的所有句柄。这些被监视对象的句柄享有平等的优先权,任何一个句柄都不可能比其他句柄具有更高的优先权。WaitForMultipleObjects()的函数原型为:
1 DWORD WaitForMultipleObjects( 2 DWORD nCount, // 等待句柄数 3 CONST HANDLE *lpHandles, // 句柄数组首地址 4 BOOL fWaitAll, // 等待标志 5 DWORD dwMilliseconds // 等待时间间隔 6 );
参数nCount指定了要等待的内核对象的数目,存放这些内核对象的数组由lpHandles来指向。fWaitAll对指定的这nCount个内核对象的两种等待方式进行了指定,为TRUE时当所有对象都被通知时函数才会返回,为FALSE则只要其中任何一个得到通知就可以返回。dwMilliseconds在饫锏淖饔糜朐赪aitForSingleObject()中的作用是完全一致的。如果等待超时,函数将返回WAIT_TIMEOUT。如果返回WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1中的某个值,则说明所有指定对象的状态均为已通知状态(当fWaitAll为TRUE时)或是用以减去WAIT_OBJECT_0而得到发生通知的对象的索引(当fWaitAll为FALSE时)。如果返回值在WAIT_ABANDONED_0与WAIT_ABANDONED_0+nCount-1之间,则表示所有指定对象的状态均为已通知,且其中至少有一个对象是被丢弃的互斥对象(当fWaitAll为TRUE时),或是用以减去WAIT_OBJECT_0表示一个等待正常结束的互斥对象的索引(当fWaitAll为FALSE时)。 下面给出的代码主要展示了对WaitForMultipleObjects()函数的使用。通过对两个事件内核对象的等待来控制线程任务的执行与中途退出:
1 // 存放事件句柄的数组 2 HANDLE hEvents[2]; 3 UINT ThreadProc14(LPVOID pParam) 4 { 5 // 等待开启事件 6 DWORD dwRet1 = WaitForMultipleObjects(2, hEvents, FALSE, INFINITE); 7 // 如果开启事件到达则线程开始执行任务 8 if (dwRet1 == WAIT_OBJECT_0) 9 { 10 AfxMessageBox("线程开始工作!"); 11 while (true) 12 { 13 for (int i = 0; i < 10000; i++); 14 // 在任务处理过程中等待结束事件 15 DWORD dwRet2 = WaitForMultipleObjects(2, hEvents, FALSE, 0); 16 // 如果结束事件置位则立即终止任务的执行 17 if (dwRet2 == WAIT_OBJECT_0 + 1) 18 break; 19 } 20 } 21 AfxMessageBox("线程退出!"); 22 return 0; 23 } 24 …… 25 void CSample08View::OnStartEvent() 26 { 27 // 创建线程 28 for (int i = 0; i < 2; i++) 29 hEvents[i] = CreateEvent(NULL, FALSE, FALSE, NULL); 30 // 开启线程 31 AfxBeginThread(ThreadProc14, NULL); 32 // 设置事件0(开启事件) 33 SetEvent(hEvents[0]); 34 } 35 void CSample08View::OnEndevent() 36 { 37 // 设置事件1(结束事件) 38 SetEvent(hEvents[1]); 39 }
MFC事件
MFC为事件相关处理也提供了一个CEvent类,共包含有除构造函数外的4个成员函数PulseEvent()、ResetEvent()、SetEvent()和UnLock()。在功能上分别相当与Win32 API的PulseEvent()、ResetEvent()、SetEvent()和CloseHandle()等函数。而构造函数则履行了原CreateEvent()函数创建事件对象的职责,其函数原型为:
1 CEvent(BOOL bInitiallyOwn = FALSE, BOOL bManualReset = FALSE, LPCTSTR lpszName = NULL, LPSECURITY_ATTRIBUTES lpsaAttribute = NULL );
图2 CEvent类对线程的同步过程示意
B线程在执行到CEvent类成员函数Lock()时将会发生阻塞,而A线程此时则可以在没有B线程干扰的情况下对共享资源进行处理,并在处理完成后通过成员函数SetEvent()向B发出事件,使其被释放,得以对A先前已处理完毕的共享资源进行操作。可见,使用CEvent类对线程的同步方法与通过API函数进行线程同步的处理方法是基本一致的。前面的API处理代码可用CEvent类将其改写为:
1 // MFC事件类对象 2 CEvent g_clsEvent; 3 UINT ThreadProc22(LPVOID pParam) 4 { 5 // 对共享资源进行写入操作 6 for (int i = 0; i < 10; i++) 7 { 8 g_cArray[i] = 'a'; 9 Sleep(1); 10 } 11 // 事件置位 12 g_clsEvent.SetEvent(); 13 return 0; 14 } 15 UINT ThreadProc23(LPVOID pParam) 16 { 17 // 等待事件 18 g_clsEvent.Lock(); 19 // 对共享资源进行写入操作 20 for (int i = 0; i < 10; i++) 21 { 22 g_cArray[10 - i - 1] = 'b'; 23 Sleep(1); 24 } 25 return 0; 26 } 27 …… 28 void CSample08View::OnEventMfc() 29 { 30 // 启动线程 31 AfxBeginThread(ThreadProc22, NULL); 32 AfxBeginThread(ThreadProc23, NULL); 33 // 等待计算完毕 34 Sleep(300); 35 // 报告计算结果 36 CString sResult = CString(g_cArray); 37 AfxMessageBox(sResult); 38 }