1、互斥量内核对象
互斥量内核对象用来确保一个线程独占对一个资源的访问。互斥量对象包含一个使用计数、线程ID以及递归计数。互斥量与关键段的行为完全相同。但是互斥量是内核对象,而关键段是用户模式下的同步对象。这意味着互斥量比关键段慢。但这同时意味着不同进程中的线程可以访问同一互斥量,还意味着线程可以在等待对资源的访问权的同时指定一个最长等待时间。
线程ID用来标识当前占用这个互斥量的是系统中的哪个线程,递归计数表示这个线程占用该互斥量的次数。互斥量一般用来对多个线程访问同一块内存进行保护。它可以确保正在访问的内存块的任何线程会独占内存块的访问权。
互斥量的规则:
- 如果线程ID为0(无效线程ID),那么该互斥量不为任何线程所占用,它处于触发状态。
- 如果线程ID为非零值,那么有一个线程已经占用了该信号量,它处于未触发状态。
- 于所有其他内核对象不同,操作系统对互斥量进行了特殊处理,允许它们违反一些常规规则。
要使用互斥量,进程必须先调用CreateMutex来创建一个互斥量
1 HANDLE CreateMutex( 2 __in_opt LPSECURITY_ATTRIBUTES lpMutexAttributes, 3 __in BOOL bInitialOwner, 4 __in_opt LPCSTR lpName 5 );
参数bInitialOwner用来控制互斥量的初始状态。如果传的是FALSE(通常情况) 那么互斥量对象的线程ID和递归计数都被设为0。这意味着互斥量不为任何线程所占用,因此处于触发状态。
如果该参数为TRUE,那么对象的线程ID被设定为调用线程的线程ID,递归计数将被设定为1。由于线程ID为非零值,因此互斥量最初处于未触发状态。
下面就正常情况给出一个例子说明互斥量的常规使用方法:(例子在VS2010下编译运行)
首先编写main函数
1 #include <stdio.h> 2 #include <Windows.h> 3 #include <process.h> 4 5 int main(int argc, char* argv[]) 6 { 7 HANDLE hMutex = NULL; 8 hMutex = CreateMutexA(NULL,FALSE,"test1234qwer"); 9 HANDLE hThread1 = CreateThread(NULL, 0, LisDevProc1, (LPVOID)&hMutex, 0, NULL); 10 HANDLE hThread2 = CreateThread(NULL, 0, LisDevProc2, (LPVOID)&hMutex, 0, NULL); 11 12 Sleep(10000); 13 14 if(hThread1) 15 CloseHandle( hThread1 ); 16 17 if(hThread2) 18 CloseHandle( hThread2 ); 19 20 CloseHandle(hMutex); 21 22 system("pause"); 23 return 0; 24 }
该函数创建了1个互斥量及两个线程,然后等待10s释放相关资源结束。
1 DWORD WINAPI LisDevProc1(LPVOID para) 2 { 3 HANDLE* phMutex = (HANDLE*)para; 4 5 WaitForSingleObject(*phMutex,INFINITE); 6 7 printf("Enter Thread1\n"); 8 printf("I'm sleeping……\n"); 9 10 Sleep(3000); 11 12 printf("Leave Thread1\n"); 13 14 ReleaseMutex(*phMutex); 15 return 0; 16 }
上述是线程1的处理函数,先等待该互斥量然后sleep3秒然后释放该互斥量。
线程2处理函数跟线程1相同,如下
1 DWORD WINAPI LisDevProc2(LPVOID para) 2 { 3 HANDLE* phMutex = (HANDLE*)para; 4 5 WaitForSingleObject(*phMutex,INFINITE); 6 7 printf("Enter Thread2\n"); 8 printf("I'm sleeping……\n"); 9 10 Sleep(3000); 11 12 printf("Leave Thread2\n"); 13 14 ReleaseMutex(*phMutex); 15 return 0; 16 }
执行结果:
执行顺序是:线程1先通过WaitForSingleObject获取互斥量的所有权,打印Enter,然后等待3秒,打印Leave,最后释放互斥量的所有权,然后线程2才获取到互斥量的所有权。。。可以看到,互斥量确实实现了对共享资源的保护。
上边还提到了一条特殊的规则,
线程在试图等待一个未触发的互斥量对象时,通常线程会进入等待状态。但是,系统会检查想要获得互斥量的线程的线程ID于互斥量内部记录的线程ID是否相同,如果线程ID一致,那么系统会让线程保持可调度状态——即使该互斥量尚未触发。每次线程成功的等待了一个互斥量,互斥量的递归计数会递增。使递归计数大于1的唯一途径就是利用这一例外,让线程多次等待同一互斥量。
对上述特例,先把以上代码中线程1的代码改为如下:
1 DWORD WINAPI LisDevProc1(LPVOID para) 2 { 3 HANDLE* phMutex = (HANDLE*)para; 4 5 WaitForSingleObject(*phMutex,INFINITE); 6 WaitForSingleObject(*phMutex,INFINITE); 7 printf("Enter Thread1\n"); 8 printf("I'm sleeping……\n"); 9 10 Sleep(3000); 11 12 printf("Leave Thread1\n"); 13 ReleaseMutex(*phMutex); 14 ReleaseMutex(*phMutex); 15 return 0; 16 }
可以看到,线程1的处理函数调用了两次WaitForSingleObject,结果跟之前结果一样,印证了该特例确实存在。
以上说来了这么多,才刚刚进入主题,讨论一下遗弃问题。
互斥量的这种线程所有权的概念导致出现遗弃问题。
如果占用互斥量的线程在释放互斥量之前终止(使用ExitThread,TerminateThread,ExitProcess,TerminateProcess)那么对于互斥量和正在等待该互斥量的线程来说会发生什么情况?答案是系统会认为互斥量被遗弃(abandoned),因为占用它的线程已经终止,因此无法释放它。
因为系统会记录所有的互斥量和线程内核对象,因此它确切的知道互斥量何时被遗弃。当互斥量被遗弃的时候,系统会自动将互斥量的线程ID设为0,将它的递归计数设为0。然后系统检查有没有其他线程正在等待该互斥量。如果有,那么系统会公平的选择一个正在等待的线程,把对象内部的线程Id设为所选择的那个线程的线程ID,并将递归计数设为1,这样被选择的线程就变成可调度状态了。
一旦检测到某互斥量被检测到,则WaitForSingleObject返回的不是WAIT_OBJECT_0,而是一个特殊值WAIT_ABANDONED。
返回该值,说明等待的互斥量被某个线程遗弃,同时说明被保护的资源已经被破坏了。这种情况下,写的程序自己必须决定该怎么做。
看下一下程序代码:
1 #include <stdio.h> 2 #include <Windows.h> 3 #include <process.h> 4 5 6 DWORD WINAPI LisDevProc1(LPVOID para) 7 { 8 HANDLE* phMutex = (HANDLE*)para; 9 10 WaitForSingleObject(*phMutex,INFINITE); 11 printf("Enter Thread1\n"); 12 printf("I'm sleeping……\n"); 13 14 Sleep(3000); 15 16 printf("Leave Thread1\n"); 17 ReleaseMutex(*phMutex); 18 return 0; 19 } 20 21 DWORD WINAPI LisDevProc2(LPVOID para) 22 { 23 24 HANDLE* phMutex = (HANDLE*)para; 25 int ret; 26 int flag; 27 do{ 28 flag = 0; 29 ret = WaitForSingleObject(*phMutex,INFINITE); 30 switch(ret) 31 { 32 case WAIT_OBJECT_0: 33 printf("normal ....\n"); 34 break; 35 case WAIT_ABANDONED: 36 flag = 1; 37 printf("abandoned ....\n"); 38 break; 39 } 40 }while(flag); 41 printf("Enter Thread2\n"); 42 printf("I'm sleeping……\n"); 43 44 Sleep(3000); 45 46 printf("Leave Thread2\n"); 47 ReleaseMutex(*phMutex); 48 return 0; 49 } 50 51 int main(int argc, char* argv[]) 52 { 53 HANDLE hMutex = NULL; 54 hMutex = CreateMutexA(NULL,FALSE,"test1234qwer"); 55 HANDLE hThread1 = CreateThread(NULL, 0, LisDevProc1, (LPVOID)&hMutex, 0, NULL); 56 HANDLE hThread2 = CreateThread(NULL, 0, LisDevProc2, (LPVOID)&hMutex, 0, NULL); 57 58 Sleep(1500); 59 TerminateThread(hThread1, 0); 60 61 Sleep(10000); 62 63 if(hThread1) 64 CloseHandle( hThread1 ); 65 66 if(hThread2) 67 CloseHandle( hThread2 ); 68 69 CloseHandle(hMutex); 70 71 system("pause"); 72 return 0; 73 }
在main函数中,创建了2个线程后的1.5秒 执行了一句TerminateThread结束了线程1,线程1没来得及释放互斥量就挂掉了,(慎重用TerminateThread等函数)看下结果如下:
可以看到在杀死线程1后,线程2的WaitForSingleObject立刻返回WAIT_ABANDONED,然后线程2再次WaitForSingleObject时又立刻返回WAIT_OBJECT_0
最后,一定注意遗弃问题的产生,如果产生,说明受保护的共享数据可能已经被破坏掉了。