## 线程同步 在C++创建多线程的程序下,可能会出现资源死锁等特殊情况的出现。所以在WIN32下存在四种同步机制——`互斥量`、`事件对象`、`信号量对象`、`关键代码段`。 四种方式: | - | 互斥量(Mutex) | 事件对象(Event) | 信号量(Semaphore) | 关键代码段(CriticalSection) | | -------------------- | ------------- | --------------- | ----------------- | --------------------------- | | 是否为内核对象 | √ | √ | √ | × | | 速度 | 较慢 | 较慢 | 较慢 | 快 | | 多个线程中的线程同步 | 支持 | 支持 | 支持 | 不支持 | | 发生死锁 | 否 | 否 | 否 | 是 | 1、互斥对象:属于内核对象,他确保线程拥有对单个资源的互斥访问权 ①、创建互斥对象:调用函数CreateMutex。调用成功,该函数返回所创建互斥对象的句柄。 ②、请求互斥对象所有权:调用函数WaitSingleObject函数。线程必须主动请求共享对象的所有权才能获得所有权。 ③、释放指定互斥对象的所有权:调用ReleaseMutex函数,线程访问共享资源结束后,线程要主动释放互斥对象的所有权。使该对象处于已通知状态。 ```c++ HANDLE WINAPI CreateMutex( _In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, //指向安全属性 _In_ BOOL bInitialOwner, //初始化互斥对象的所有者,TRUE立即拥有互斥体 _In_opt_ LPCWSTR lpName //指向互斥对象的指针 ); BOOL WINAPI ReleaMutex( HANDLE hMutex //指定一个互斥体的句柄 ); //返回值:TRUE代表成功,FLASE失败 BOOL CloseHandle(HANDLE hObject) ( hObject:代表一个已打开对象handle ); //返回值:TRUE执行成功 FALSE:执行失败,可以调用GetLastError()获得失败原因。 //CreateMutex及ReleaseMutexing及CloseHandle应用实例 HANDLE hMutex; //全局变量 int main() { hMutex = CreateMutex(NULL,FALSE,NULL); //多线程代码实现 CloseHandle(hMutex); return 0; } unsigned WINAPI threadInc(void* atg) { int i; WaitForSingleObject(hMutex,INFINITE); //实现代码 ReleaseMutex(hMutex); return 0; } ``` 2、事件对象:事件对象也属于内核对象(包含:1、使用计数;2、用于指明该事件是一个自动重置的事件还是一个人工重置的BOOL值;3、用于指明该时间处于已通知状态还是未通知状态的BOLL值)。 1、创建事件对象:调用CreateEvent函数创建或打开一个命名的或匿名的事件对象 2、设置事件对象状态:调用SetEvent函数把指定的事件对象设置为有信号状态 3、重置事件对象状态:调用ReseEvent函数把指定的事件对象设置为无信号状态 4、请求时间对象:线程通过调用WaitForSingleObject函数请求事件对象 > 事件对象有两种类型:人工重置的事件对象和自动重置的事件对象。这两种事件对象的区别在于当人工重置的事件对象得到通知时,等待该事件对象的所有进程均变为可调度线程;而当一个自动重置的事件对象得到通知时,等待该时间对象的线程中只有一个线程变为可调度线程。 ```c++ // 创建事件对象的函数原型: HANDLE CreateEvent(    LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性    BOOL bManualReset,   //复位方式  TRUE 必须用ResetEvent手动复原 FALSE 自动还原为无信号状态 BOOL bInitialState,  //初始状态   TRUE 初始状态为有信号状态 FALSE 无信号状态 LPCTSTR lpName     //对象名称  NULL 无名的事件对象  ); ``` 3、`信号量`:内核对象状态:1、触发状态(有信号状态:表示有可用资源)。2、未触发状态(无信号状态:没有可用资源) 信号量规则: ①、如果当前资源计数大于0,那么信号量处于触发状态,表示有可用资源 ②、系统绝不会让当前资源计数变为负数 ③、当前资源计数绝不会大于最大资源计数 信号量与互斥量不同的是它允许多个线程同一时刻访问同一资源,但是要规定访问的数量。 ```c++ //创建信号量 HANDLE CreateSemaphoreW( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, //NULL安全属性 LONG lInitialCount, //最多有多少个资源是可用的 LONG lMaximumCount, //信号量对象的最大计数 LPCWSTR lpName ); //NULL 信号量的名称 /*返回值:如果函数成功,返回值是信号量对象的句柄。如果在函数调用之前存在命名的信号量对象,则该函数将返回现有对象的句柄。而GetLastError将返回ERROR_ALREADY_EXISTS 如果失败,则返回值为NULL。 */ //增加信号量 BOOL ReleaseSemaphore( HANDLE hSemaphore, //信号量对象的句柄 LONG lReleaseCount, //信号量对象当前计数要增加的数量 LPLONG lpPreviousCount //当前资源计数的原始值 ); //关闭句柄 BOOL CloseHandle( HANDLE hObject //打开对象的有效句柄); ``` 4、`关键代码段`:也称为`临界区`,工作在用户方式下。它是指一个小代码段,在代码能够执行前,他必须独占对某些资源的访问权。(通常把多线程访问同一种资源的那部分代码当作关键代码段) ```c++ //1.初始化一个关键代码段 void InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection //指向关键代码段的指针); //2.等待指定关键部分对象的所有权 void EnterCriticalSection( LPCRITICAL_SECTION lpCriticalSection //指向关键部分对象的指针); /*调用EnterCriticalSection函数获得指定的临界区对象的所有权,该函数等待指定的临界区对象的所有权,如果该所有权赋予了调用线程,则该函数就返回;否则该函数就会一直等待导致线程等待*/ //3.释放指定关键部分对象的所有权 void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection //指向关键部分对象的指针); //4.删除临界区 void LeaveCriticalSection( LPCRITICAL_SECTION lpCriticalSection //指向关键部分对象的指针 /*释放一个没有被任何线程所拥有的临界区对象的所有资源*/ ); ``` 线程死锁:死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,进程都无法向前推进。 避免死锁的方式: ①、让程序每次至多只能获得一个锁,多线程情况下很难实现不容易实现 ②、设计时考虑清除锁的顺序,尽量减少嵌在的枷锁交互数量 ③、设置等待时间的上限,超过等待时间上限返回一个失败信息。 ## 多线程 注意:CloseHandle是关闭线程句柄,用来释放线程资源的,并不是终止线程的。关闭线程句柄只是释放句柄资源,新开启线程后,如果不在利用其句柄,应该关闭句柄,释放系统资源。关闭线程句柄和线程的结束与否没有关系。如果主线程只是想创建线程,而并不想之后再查询或操纵它,那么及时关闭句柄是个好习惯,免得当时没关后面又忘了,这样的后果是泄露了系统的句柄资源,系统的句柄总数是有一定限度的。 Windows操作系统会强迫让等待中的线程有轮番更替的机会,对于所有的同步机制,这都是一个很重要的行为。如果操作系统没有强迫实现某种层次的公平性,可能就会有某个线程不断获得执行机会,而某个线程一直能获得 CPU 的青睐。这种情况被称为饥饿。 事件——event 1、通知类型 ```c++ HANDLE CreateEvent( LPSECURITY_ATTRBUTES lpEventAttributes, //继承安全,默认设置为 NULL BOOL bManualReset, //TRUE 手工重置信号;FALSE 系统自动重置信号 BOOL bInitialState, //初始状态 LPCTSTR lpName //对象名称,NULL表示无名事件对象 ) ``` 参数说明: > lpEventAttributes:指向SECURITY_ATTRIBUTES结构体,此结构体决定函数的返回句柄是否可以让子进程继承。如果这个参数为NULL,这个句柄是不能继承的。一般情况下,这个参数设置为NULL。 > > bManualReset:指定将创建的EVENT是自动复位还是手动复位。如果为TRUE,需要用ResetEvent(HANDLE)函数手动复位状态为无信号,即一旦改EVENT被设置成有信号,则它会一直等到ResetEvent调用时才为无信号状态。如果为FALSE,当一个有信号的等待线程被释放后,系统会自动复位状态为无信号状态。 > > bInitialState:指定事件对象的初始状态。如果为TRUE,初始状态为有信号,否则为无信号。 > > lpName: 事件对象的名称,以字符串表示。名称的长度受MAX_PATH的限制,名称是大小写敏感的。如果lpName匹配一个存在的命名的事件对象,函数将请求EVENT_ALL_ACCESS来访问存在的对象。在这种情况下,bManualReset和bInitialState 被忽略,因为这两个参数已经被存在的事件设置。如果lpEventAttributes参数不为NULL,这个参数可以决定是否句柄被继承,但是它的安全描述(security-descriptor)成员被忽略。如果lpName 为NULL,创建一个没有名称的事件。如果lpName 匹配一个存在的semaphore, mutex, waitable timer, job或者file-mapping对象的名称,函数调用失败,GetLastError函数返回ERROR_INVALID_HANDLE。由于这些对象共享相同的命名空间,才导致这种情况的发生。 > > 返回值: 函数返回句柄,该句柄具有EVENT_ALL_ACCESS权限去访问新的事件对象,同时它可以在任何需要事件对象句柄的函数中使用。 调用过程中的任何线程,都可以在一个等待函数中指定事件对象句柄。当指定的对象状态为有信号时,单对象等待函数(例如 WaitForSingleObject)返回。对于多对象等待函数(例如WaitForMultipleObjects),可以指定为任意或所有指定的对象被置为有信号状态。当等待函数返回时,等待线程将被释放去继续它的执行。 事件对象的初始状态由bInitialState参数指定,用SetEvent函数可以设置对象为有信号状态,用ResetEvent函数可以设置对象为无信号状态。 当一个手动复原的事件对象的状态被置为有信号状态时,该对象将一直保持有信号状态,直至明确调用ResetEvent函数将其置为无符号状态。当事件对象被设置为有信号状态时,任何数量的等待线程或者随后等待的线程都会被释放。 当一个自动复原事件对象的状态被设置为有信号状态时,该对象一直保持有信号状态,直至一个单等待线程被释放;系统然后会自动重置对象到无信号状态。 多个进程可持有同一个事件对象的多个句柄,可以通过使用此对象来实现进程间的同步。下面的对象共享机制是可行的: 在CreateEvent函数中,lpEventAttributes参数指定句柄可被继承时,通过CreateProcess函数创建的子进程继承的事件对象句柄。 一个进程可以在DuplicateHandle函数中指定事件对象句柄,从而获得一个复制的句柄,此句柄可以被其它进程使用。 一个进程可以在OpenEvent或CreateEvent函数中指定一个名字,从而获得一个有名的事件对象句柄。(在调用OpenEvent或CreateEvent函数时,一个进程可以指定事件对象的名字。) 使用CloseHandle函数关闭句柄。当进程终止时,系统将自动关闭句柄。事件对象会被销毁,当最后一个句柄被关闭。 示例1 ```c++ #include "iostream" #include "windows.h" using namespace std; DWORD WINAPI ThreadProc1(LPVOID lpParam); DWORD WINAPI ThreadProc2(LPVOID lpParam); HANDLE hEvent = NULL; HANDLE hThread1 = NULL; HANDLE hThread2 = NULL; int main(int argc, char *args[]) { hEvent = CreateEvent(NULL, TRUE, TRUE, NULL); //手动重置才能无信号状态 hThread1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc1, NULL, 0, NULL); Sleep(200); hThread2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc2, NULL, 0, NULL); Sleep(200); if (hThread1 == NULL) { cout << "create thread fail!"; } return 0; } DWORD WINAPI ThreadProc1(LPVOID lpParam) { cout << "in the thread1@!" << endl; DWORD dReturn = WaitForSingleObject(hEvent, INFINITE); if (WAIT_OBJECT_0 == dReturn) { cout << "thread1 singnaled!" << endl; } cout << "in thread1 --signal" << endl; return 0; } DWORD WINAPI ThreadProc2(LPVOID lpParam) { cout << "in the thread2@" << endl; DWORD dReturn = WaitForSingleObject(hEvent, INFINITE); if (WAIT_OBJECT_0 == dReturn) { cout << "thread2 singnaled!" << endl; } cout << "in thread2 --signal" << endl; return 0; } ``` 执行完线程1后又执行完线程2,手动重置为无信号状态,初始化时有信号状态,所以 hEvent 一直处于有信号状态。无论线程1释放后,hEvent 依旧处于有信号状态,所以线程2正常执行。 示例2 ```c++ #include "iostream" #include "windows.h" using namespace std; DWORD WINAPI ThreadProc1(LPVOID lpParam); DWORD WINAPI ThreadProc2(LPVOID lpParam); HANDLE hEvent = NULL; HANDLE hThread1 = NULL; HANDLE hThread2 = NULL; int main(int argc, char *args[]) { hEvent = CreateEvent(NULL, FALSE, TRUE, NULL); //当一个线程被释放时,系统自动重置为无信号状态,初始值为有信号状态 hThread1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc1, NULL, 0, NULL); Sleep(200); hThread2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc2, NULL, 0, NULL); Sleep(200); if (hThread1 == NULL) { cout << "create thread fail!"; } CloseHandle(hThread1); CloseHandle(hThread2); CloseHandle(hEvent); return 0; } DWORD WINAPI ThreadProc1(LPVOID lpParam) { cout << "in the thread1@!" << endl; DWORD dReturn = WaitForSingleObject(hEvent, INFINITE); if (WAIT_OBJECT_0 == dReturn) { cout << "thread1 singnaled!" << endl; } cout << "in thread1 --signal" << endl; return 0; } DWORD WINAPI ThreadProc2(LPVOID lpParam) { cout << "in the thread2@" << endl; DWORD dReturn = WaitForSingleObject(hEvent, INFINITE); if (WAIT_OBJECT_0 == dReturn) { cout << "thread2 singnaled!" << endl; } cout << "in thread2 --signal" << endl; return 0; } ``` 从结果看,执行力线程1,线程2一直在等待,直到主线程结束。当一个线程被释放后,自动重置为无信号状态,初始状态为有信号状态。初始执行 hEvent 是有信号的,所以执行了线程1而没有执行线程2。 示例3 ```c++ #include "iostream" #include "windows.h" using namespace std; DWORD WINAPI ThreadProc1(LPVOID lpParam); DWORD WINAPI ThreadProc2(LPVOID lpParam); HANDLE hEvent = NULL; HANDLE hThread1 = NULL; HANDLE hThread2 = NULL; int main(int argc, char *args[]) { hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); //初始状态无信号,这两个线程一直在等待,直到主线程执行完 hThread1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc1, NULL, 0, NULL); Sleep(200); hThread2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc2, NULL, 0, NULL); Sleep(200); if (hThread1 == NULL) { cout << "create thread fail!"; } CloseHandle(hThread1); CloseHandle(hThread2); CloseHandle(hEvent); return 0; } DWORD WINAPI ThreadProc1(LPVOID lpParam) { cout << "in the thread1@!" << endl; DWORD dReturn = WaitForSingleObject(hEvent, INFINITE); if (WAIT_OBJECT_0 == dReturn) { cout << "thread1 singnaled!" << endl; } cout << "in thread1 --signal" << endl; return 0; } DWORD WINAPI ThreadProc2(LPVOID lpParam) { cout << "in the thread2@" << endl; DWORD dReturn = WaitForSingleObject(hEvent, INFINITE); if (WAIT_OBJECT_0 == dReturn) { cout << "thread2 singnaled!" << endl; } cout << "in thread2 --signal" << endl; return 0; } ``` 因为初始为无信号状态,所以hEvent一直处于无信号状态,因此这两个线程一直在等待,直到主线程结束。 示例4 ```c++ #include "iostream" #include "windows.h" using namespace std; DWORD WINAPI ThreadProc1(LPVOID lpParam); DWORD WINAPI ThreadProc2(LPVOID lpParam); HANDLE hEvent = NULL; HANDLE hThread1 = NULL; HANDLE hThread2 = NULL; int main(int argc, char *args[]) { hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); //当一个线程被释放时,系统自动重置为无信号状态,初始值为有信号状态 if (SetEvent(hEvent)) { cout << "SetEvent 成功。" << endl; } hThread1 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc1, NULL, 0, NULL); Sleep(200); hThread2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc2, NULL, 0, NULL); Sleep(200); if (hThread1 == NULL) { cout << "create thread fail!"; } CloseHandle(hThread1); CloseHandle(hThread2); CloseHandle(hEvent); return 0; } DWORD WINAPI ThreadProc1(LPVOID lpParam) { cout << "in the thread1@!" << endl; DWORD dReturn = WaitForSingleObject(hEvent, INFINITE); if (WAIT_OBJECT_0 == dReturn) { cout << "thread1 singnaled!" << endl; } cout << "in thread1 --signal" << endl; return 0; } DWORD WINAPI ThreadProc2(LPVOID lpParam) { cout << "in the thread2@" << endl; DWORD dReturn = WaitForSingleObject(hEvent, INFINITE); if (WAIT_OBJECT_0 == dReturn) { cout << "thread2 singnaled!" << endl; } cout << "in thread2 --signal" << endl; return 0; } ``` 2、线程同步 线程互斥:线程互斥是指对于共享的进程系统资源,在各个单线程访问时的排它性。当有若干线程都要使用某一共享资源时,任何时刻最多只允许一个线程去使用,其他要使用的线程都必须等待,直到占用资源者释放该内存。 线程同步:线程同步是指多个线程之间具有一种制约关系,一个线程的执行以来另一个线程的消息,当它没有得到另一个线程的消息时应等待,直到消息到达时才被唤醒。 同步的前提是互斥。同步=互斥+有序 3、线程同步——事件 以上添加互斥锁在添加上我们自己设置的条件,实现了表面上的线程有序的进行,但是这中是浪费CPU时间的做法,通常更好的做法(不浪费CPU时间而又实现线程有序的运行)是通过事件Event。 ```c++ #include "stdafx.h" #include DWORD dwMax = 10; DWORD dwBuffer = 0; HANDLE EventProducer; HANDLE EventConsumer; //生产者线程 DWORD WINAPI ThreadProducer(LPVOID lpParameter) { for(int i=0; im_hEvent, TRUE,500); if (WAIT_OBJECT_0 + 1 == nIndex || nIndex == WAIT_OBJECT_0) //所有事件发生 { //所有的信号量都有效时 } } } ```