- 线程同步三:事件
CreateEvent:Creates or opens a named or unnamed event object.
HANDLE WINAPI CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_In_opt_ LPCTSTR lpName
);
函数说明:
第一个参数表示安全控制,一般直接传入NULL。
第二个参数确定事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。如果为自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态。
第三个参数表示事件的初始状态,传入TRUR表示已触发。
第四个参数表示事件的名称,传入NULL表示匿名事件。
OpenEvent:Opens an existing named event object.
HANDLE WINAPI OpenEvent(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ LPCTSTR lpName
);
函数说明:
第一个参数表示访问权限,对事件一般传入EVENT_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示事件句柄继承性,一般传入TRUE即可。
第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个事件。
SetEvent:Sets the specified event object to the signaled state.
BOOL WINAPI SetEvent(
_In_ HANDLE hEvent
);
函数功能:触发事件
函数说明:每次触发后,必有一个或多个处于等待状态下的线程变成可调度状态。
ResetEvent:Sets the specified event object to the nonsignaled state.
BOOL WINAPI ResetEvent(
_In_ HANDLE hEvent
);
函数功能:将事件设为末触发
CloseHandle:事件销毁
举例:与Mutex中的例子相比,这里只修改了两行代码
#include <iostream> #include <windows.h> using namespace std; long g_nNum; #define MAX_THREADS 20 HANDLE g_hThreadEvent; CRITICAL_SECTION g_csThreadCode; DWORD WINAPI MyThreadFunction(LPVOID lpParam) { int nThreadNum = *(int *)lpParam; SetEvent(g_hThreadEvent);//释放互斥量以便供其他线程获取 Sleep(500); EnterCriticalSection(&g_csThreadCode); g_nNum++; Sleep(500); cout << "参数 " << nThreadNum << " : " << "资源:" << g_nNum << endl; LeaveCriticalSection(&g_csThreadCode); return 0; } int main() { g_hThreadEvent = CreateEvent(NULL, FALSE, TRUE, NULL); InitializeCriticalSection(&g_csThreadCode); HANDLE hThreadArray[MAX_THREADS]; DWORD dwThreadIdArray[MAX_THREADS] = { 0 }; g_nNum = 0; int i = 0; while (i < MAX_THREADS) { WaitForSingleObject(g_hThreadEvent, INFINITE);//等待获取互斥量 hThreadArray[i] = (HANDLE)CreateThread(NULL, 0, MyThreadFunction, &i, 0, &dwThreadIdArray[i]); i++; } WaitForMultipleObjects(MAX_THREADS, hThreadArray, TRUE, INFINITE); //销毁互斥量和关键段 CloseHandle(g_hThreadEvent); DeleteCriticalSection(&g_csThreadCode); for (i = 0; i < MAX_THREADS; i++) CloseHandle(hThreadArray[i]); system("pause"); return 0; }
模仿CRITICAL_SECTION例子中的方式,在WaitForSingleObject和SetEvent上分别下断点:
然后这次再运行,与CRITICAL_SECTION中例子不同的现象发生了,这次会轮流在WaitForSingleObject和SetEvent处断下,而运行结果也显示:
参数不再有重复的了,说明利用Event实现了主线程和各个子线程间的同步。
但是为什么Event可以?
Mutex有一个“所有者”的概念,只有拥有互斥对象的线程才具有访问资源 的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。事件对象也可以通过通知操作的方式来保持线程的同步。可以这么理解,对于Mutex,A线程占有资源S,A可以重复多次访问资源S,除非A自身决定释放,否则其它线程无法去访问S。但是对于Event就不同了。A线程Wait将事件变成未触发状态进行它的操作,操作后,B线程可以触发这个事件,继续B的后续的操作,若无线程释放,则A也无法重复的去获取这个事件以重复进行操作。
此外,Event还有一个API:PulseEvent
BOOL WINAPI PulseEvent(
_In_ HANDLE hEvent
);
函数功能:将事件触发后立即将事件设置为未触发,相当于触发一个事件脉冲。
函数说明:这是一个不常用的事件函数,此函数相当于SetEvent()后立即调用ResetEvent();此时情况可以分为两种:
1.对于手动置位事件,所有正处于等待状态下线程都变成可调度状态。
2.对于自动置位事件,所有正处于等待状态下线程只有一个变成可调度状态。
此后事件是末触发的。该函数不稳定,因为无法预知在调用PulseEvent ()时哪些线程正处于等待状态。
举例:
设为自动置位:
#include <iostream> #include <windows.h> using namespace std; HANDLE g_hThreadEvent; //快线程 DWORD WINAPI FastThreadFun(LPVOID lpParam) { Sleep(10); //用这个来保证各线程调用等待函数的次序有一定的随机性 //cout << "启动线程" << lpParam << endl; printf("%s 启动线程\n", lpParam); WaitForSingleObject(g_hThreadEvent, INFINITE);//线程在等待时间 //自动就有个ResetEvent printf("%s 等到事件被触发 顺利结束\n", lpParam); return 0; } //慢线程 DWORD WINAPI SlowThreadFun(LPVOID lpParam) { Sleep(10); printf("%s 启动线程\n", lpParam); WaitForSingleObject(g_hThreadEvent, INFINITE); //自动就有个ResetEvent 置为未触发状态 printf("%s 等到事件被触发 顺利结束\n", lpParam); return 0; } int main() { BOOL bManualReset = FALSE;//自动置位FALSE 即自动ResetEvent(未触发状态) //第三个参数是FALSE,标明事件被初始化时,其状态是未触发的 g_hThreadEvent = CreateEvent(NULL, bManualReset, FALSE, NULL); //"数组"的数组,其实"数组"就是地址。szFastThreadName[5][30]表示5个"数组",即5个地址.szFastThreadName[i]就是一个地址 //观察内存也可发现,这个数组szFastThreadName[5],保存了5个地址,每个地址处开辟了30字节的空间 //int a[5] = { 1, 2, 3, 4, 5 };//这里每四个字节存一个数 char szFastThreadName[5][30] = { "Fast1000", "Fast1001", "Fast1002", "Fast1003", "Fast1004" };//这里每30个字节存一个字符数组 //但a[i]是第一个数字,而szFastThreadName[i]不是第i个数组,而是第i个数组的地址。开辟了5*30字节空间,但是用地址代表每个数组 //我们可以想象,a是数组a[5]的首地址;那么,szSlowThreadName[1],就是数组szSlowThreadName[1][30]的首地址 char szSlowThreadName[2][30] = { "Slow196", "Slow197" }; DWORD dwFastThreadID[5]; DWORD dwSlowThreadID[2]; int i; for (i = 0; i < 5; i++) CreateThread(NULL, 0, FastThreadFun, szFastThreadName[i], 0, &dwFastThreadID[i]); for (i = 0; i < 2; i++) CreateThread(NULL, 0, SlowThreadFun, szSlowThreadName[i], 0, &dwSlowThreadID[i]); Sleep(50); //保证快线程已经全部启动 cout << "现在主线程触发一个事件脉冲 - PulseEvent()" << endl; PulseEvent(g_hThreadEvent); //自动的话,已经有了一个ResetEvent // SetEvent(g_hThreadEvent); //触发 // ResetEvent(g_hThreadEvent); //Sleep(3000); cout << "时间到,主线程结束运行" << endl; CloseHandle(g_hThreadEvent); system("pause"); return 0; }
自动置位的思路是这样的,由于CreateEvent的第三个参数是FALSE,所以,这个事件一上来是未激活的。主线程先起来7个子线程,当主线程执行一个PulseEvent时,会在某一瞬间触发一个事件,所以会有一个正在Wait的子线程被激活而顺利执行,但由于是自动置位的,所以这个被激活的子线程在WaitForSingleObject后,就立刻自动ResetEvent了,于是其它的线程无法被激活,由于主线程没有WaitForSingleObject,所以,只有主线程继续顺利执行下去。
而如果设为手动置位结果如下:
因为,主线程PulseEvent之后,某个线程Wait到这个事件,而它在WaitForSingleObject并未ResetEvent,所以事件还是触发状态,其它的正在Wait的子线程仍可以Wait到,所以激活了全部子线程。
参考:http://blog.csdn.net/morewindows/article/details/7445233
- 线程同步四:信号量(Semaphore)
CreateSemaphore
HANDLE WINAPI CreateSemaphore(
_In_opt_ LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
_In_ LONG lInitialCount,
_In_ LONG lMaximumCount,
_In_opt_ LPCTSTR lpName
);
函数功能:创建信号量
函数说明:
第一个参数表示安全控制,一般直接传入NULL。
第二个参数表示初始资源数量。
第三个参数表示最大并发数量。
第四个参数表示信号量的名称,传入NULL表示匿名信号量。
OpenSemaphore
HANDLE WINAPI OpenSemaphore(
_In_ DWORD dwDesiredAccess,
_In_ BOOL bInheritHandle,
_In_ LPCTSTR lpName
);
函数功能:打开信号量
函数说明:
第一个参数表示访问权限,对一般传入SEMAPHORE_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示信号量句柄继承性,一般传入TRUE即可。
第三个参数表示名称,不同进程中的各线程可以通过名称来确保它们访问同一个信号量。
ReleaseSemaphore
BOOL WINAPI ReleaseSemaphore(
_In_ HANDLE hSemaphore,
_In_ LONG lReleaseCount,
_Out_opt_ LPLONG lpPreviousCount
);
函数功能:递增信号量的当前资源计数
函数说明:
第一个参数是信号量的句柄。
第二个参数表示增加个数,必须大于0且不超过最大资源数量。
第三个参数可以用来传出先前的资源计数,设为NULL表示不需要传出。
注意:当前资源数量大于0,表示信号量处于触发,等于0表示资源已经耗尽故信号量处于末触发。在对信号量调用等待函数时,等待函数(WaitForSingleObject)会检查信号量的当前资源计数,如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。
CloseHandle
最后一个 信号量的清理与销毁.由于信号量是内核对象,因此使用CloseHandle()就可以完成清理与销毁了。
在经典多线程问题中设置一个信号量和一个关键段。用信号量处理主线程与子线程的同步,用关键段来处理各子线程间的互斥。举例:
#include <stdio.h> #include <windows.h> long g_nNum; const int THREAD_NUM = 10; //信号量与关键段 HANDLE g_hThreadParameter; CRITICAL_SECTION g_csThreadCode; DWORD WINAPI ChildThread(LPVOID lpParam) { int nThreadNum = *(int *)lpParam; ReleaseSemaphore(g_hThreadParameter, 1, NULL);//信号量++ 变为1,主线程中的WaitForSingleObject //才能Wait到信号量,从而继续的执行 Sleep(50); EnterCriticalSection(&g_csThreadCode); ++g_nNum; Sleep(0); printf("线程:%d 资源:%d\n", nThreadNum, g_nNum); LeaveCriticalSection(&g_csThreadCode); return 0; } int main() { //初始化信号量和关键段 g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL);//初始0个资源,最大允许1个同时访问 InitializeCriticalSection(&g_csThreadCode); HANDLE handle[THREAD_NUM]; g_nNum = 0; int i = 0; while (i < THREAD_NUM) { handle[i] = (HANDLE)CreateThread(NULL, 0, ChildThread, &i, 0, NULL); WaitForSingleObject(g_hThreadParameter, INFINITE);//等待信号量>0 ++i; } WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); DeleteCriticalSection(&g_csThreadCode); CloseHandle(g_hThreadParameter); for (i = 0; i < THREAD_NUM; i++) CloseHandle(handle[i]); system("pause"); return 0; }
CreateSemaphore的第二个参数位0,第三个参数为1,表示信号量初始为0,最多允许1个线程同时访问。主线程中CreateThread之后,信号量个数为0,所以主线程只能一直Wait,直到子线程中有一个ReleaseSemaphore是的信号量个数加1大于0,
这时主线程才能Wait到信号量,且自动减1。而后边主线程如果再想执行,也必须要等到其它子线程先ReleaseSemaphore才行。
可以看出来,信号量也可以解决线程之间的同步问题。