【多线程】学习6
以下内容来自:http://blog.csdn.net/morewindows/article/details/7470936
前面介绍了关键段CS、事件Event在经典线程同步问题中的使用。本篇介绍用互斥量Mutex来解决这个问题。
互斥量也是一个内核对象,它用来确保一个线程独占一个资源的访问。互斥量与关键段的行为非常相似,并且互斥量可以用于不同进程中的线程互斥访问资源。使用互斥量Mutex主要将用到四个函数。下面是这些函数的原型和使用说明。
第一个 CreateMutex
函数功能:创建互斥量(注意与事件Event的创建函数对比)
函数原型:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);
函数说明:
第一个参数表示安全控制,一般直接传入NULL。
第二个参数用来确定互斥量的初始拥有者。如果传入TRUE表示互斥量对象内部会记录创建它的线程的线程ID号并将递归计数设置为1,由于该线程ID非零,所以互斥量处于未触发状态。如果传入FALSE,那么互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这意味互斥量不为任何线程占用,处于触发状态。
第三个参数用来设置互斥量的名称,在多个进程中的线程就是通过名称来确保它们访问的是同一个互斥量。
函数访问值:
成功返回一个表示互斥量的句柄,失败返回NULL。
第二个打开互斥量
函数原型:
HANDLE OpenMutex(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
LPCTSTR lpName //名称
);
函数说明:
第一个参数表示访问权限,对互斥量一般传入MUTEX_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示互斥量句柄继承性,一般传入TRUE即可。
第三个参数表示名称。某一个进程中的线程创建互斥量后,其它进程中的线程就可以通过这个函数来找到这个互斥量。
函数访问值:
成功返回一个表示互斥量的句柄,失败返回NULL。
第三个触发互斥量
函数原型:
BOOL ReleaseMutex (HANDLE hMutex)
函数说明:
访问互斥资源前应该要调用等待函数,结束访问时就要调用ReleaseMutex()来表示自己已经结束访问,其它线程可以开始访问了。
最后一个清理互斥量
由于互斥量是内核对象,因此使用CloseHandle()就可以(这一点所有内核对象都一样)。
接下来我们就尝试在经典多线程问题用互斥量来保证主线程与子线程之间的同步,由于互斥量的使用函数类似于事件Event,所以可以仿照上一篇的实现来写出代码:
#include <stdio.h> #include <process.h> #include <Windows.h> long g_nNum; unsigned int __stdcall Fun(void *pPM); const int THREAD_NUM = 10; //互斥量与关键段 HANDLE g_hThreadParameter; CRITICAL_SECTION g_csThreadCode; int main() { //初始化互斥量与关键段 第二个参数为TRUE表示互斥量为创建线程所有 g_hThreadParameter = CreateMutex(NULL, FALSE, NULL); InitializeCriticalSection(&g_csThreadCode); HANDLE handle[THREAD_NUM]; g_nNum = 0; int i = 0; while(i < THREAD_NUM) { handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL); WaitForSingleObject(g_hThreadParameter, INFINITE); i++; } WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE); //销毁互斥量和关键段 CloseHandle(g_hThreadParameter); DeleteCriticalSection(&g_csThreadCode); for (i = 0; i < THREAD_NUM; i++) CloseHandle(handle[i]); return 0; } unsigned int __stdcall Fun(void *pPM) { int nThreadNum = *(int *)pPM; ReleaseMutex(g_hThreadParameter);//触发互斥量 Sleep(50);//some work should to do EnterCriticalSection(&g_csThreadCode); g_nNum++; Sleep(0);//some work should to do printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum); LeaveCriticalSection(&g_csThreadCode); return 0; }
可以看出,与关键段类似,互斥量也是不能解决线程间的同步问题。
联想到关键段会记录线程ID即有“线程拥有权”的,而互斥量也记录线程ID,莫非它也有“线程拥有权”这一说法。
答案确实如此,互斥量也是有“线程拥有权”概念的。“线程拥有权”在关键段中有详细的说明,这里就不再赘述了。另外由于互斥量常用于多进程之间的线程互斥,所以它比关键段还多一个很有用的特性——“遗弃”情况的处理。比如有一个占用互斥量的线程在调用ReleaseMutex()触发互斥量前就意外终止了(相当于该互斥量被“遗弃”了),那么所有等待这个互斥量的线程是否会由于该互斥量无法被触发而陷入一个无穷的等待过程中了?这显然不合理。因为占用某个互斥量的线程既然终止了那足以证明它不再使用被该互斥量保护的资源,所以这些资源完全并且应当被其它线程来使用。因此在这种“遗弃”情况下,系统自动把该互斥量内部的线程ID设置为0,并将它的递归计数器复置为0,表示这个互斥量被触发了。然后系统将“公平地”选定一个等待线程来完成调度(被选中的线程的WaitForSingleObject()会返回WAIT_ABANDONED_0)。
下面写二个程序来验证下:
第一个程序创建互斥量并等待用户输入后就触发互斥量。第二个程序先打开互斥量,成功后就等待并根据等待结果作相应的输出。详见代码:
第一个程序:
#include <stdio.h> #include <conio.h> #include <windows.h> const char MUTEX_NAME[] = "Mutex"; int main() { HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEX_NAME); //创建互斥量 printf("互斥量已经创建,现在按任意键触发互斥量\n"); getch(); //exit(0); ReleaseMutex(hMutex); printf("互斥量已经被触发,可以被其他线程调用\n"); CloseHandle(hMutex); return 0; }
第二个程序:
#include <stdio.h> #include <windows.h> const char MUTEX_NAME[] = "Mutex"; int main() { HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, TRUE, MUTEX_NAME); //打开互斥量 if(hMutex == NULL) { printf("打开互斥量失败\n"); return 0; } printf("等待中....\n"); DWORD dwResult = WaitForSingleObject(hMutex, 20 * 1000); //等待互斥量被触发 switch (dwResult) { case WAIT_ABANDONED: printf("拥有互斥量的进程意外终止\n"); break; case WAIT_OBJECT_0: printf("已经收到信号\n"); break; case WAIT_TIMEOUT: printf("信号未在规定的时间内送到\n"); break; } CloseHandle(hMutex); return 0; }
运用这二个程序时要先启动程序一再启动程序二。下面展示部分输出结果:
结果一.二个进程顺利执行完毕:
结果二.将程序一中//exit(0);前面的注释符号去掉,这样程序一在触发互斥量之前就会因为执行exit(0);语句而且退出,程序二会收到WAIT_ABANDONED消息并输出“拥有互斥量的进程意外终止”:
有这个对“遗弃”问题的处理,在多进程中的线程同步也可以放心的使用互斥量。
----------------------------------------------------------------------------------------
关于CloseHandle的问题:
第一个代码里面,触发后就使用了CloseHandle(hMutex); 令我很疑惑,互斥量都销毁了,怎么在另一个进程中使用呢?
后来从网上找了一个解释:http://blog.chinaunix.net/uid-22156506-id-401460.html
closehandle()并没有真正的销毁句柄
具体解释:
每次createthread()创建线程对象的时候,线程对象中Usage count的初始化值为2(注意不是1)closehandle()能是Usage count的值减少1,这个时候Usage count的值为1,所以并没有销毁,只有当线程执行的函数通过return结束的时候,Usage count继续减少1变为0,这个时候才真正的销毁对象。
------------------------------------------------------------------------------
最后总结下互斥量Mutex:
1.互斥量是内核对象,它与关键段都有“线程所有权”所以不能用于线程的同步。
2.互斥量能够用于多个进程之间线程互斥问题,并且能完美的解决某进程意外终止所造成的“遗弃”问题。
---------------------------------------------------------------------------
不懂!互斥量有所有权,互斥量是在子线程中触发的,那所有权应该是子线程吧?每个子线程都只进入了一次,为什么会输出多个相同的值?
触发和未触发的关系是什么?Event没有所有权吗?Event有set和release 而Mutex只有release 它们是怎么对应的? 究竟触发时变量为所有线程可用还是未触发的时候对所有线程可用?
自己给自己的答案:互斥量的所有权如果没有指定那么应该是归加锁的线程 也就是WaitForSingleObject的线程 在上例中 因为所有权在主线程 所以可以多次的对自己加锁 但这样是错误的写法 不应该连续加锁两次。
触发就是解锁后的状态 未触发就是加锁后的状态 Mutex的release与WaitForSingleObject对应 解锁后所有线程可用
---------------------------------------------------------
又做了个实验 把代码中的创建子线程去掉,看看直接等待互斥量会怎样。
#include <stdio.h> #include <process.h> #include <Windows.h> long g_nNum; unsigned int __stdcall Fun(void *pPM); const int THREAD_NUM = 10; //互斥量与关键段 HANDLE g_hThreadParameter; CRITICAL_SECTION g_csThreadCode; int main() { //初始化互斥量与关键段 第二个参数为TRUE表示互斥量为创建线程所有 g_hThreadParameter = CreateMutex(NULL, FALSE, NULL); InitializeCriticalSection(&g_csThreadCode); int i = 0; while(i < THREAD_NUM) { WaitForSingleObject(g_hThreadParameter, INFINITE); printf("等待到了%d个互斥量\n", i); i++; } //销毁互斥量和关键段 CloseHandle(g_hThreadParameter); DeleteCriticalSection(&g_csThreadCode); return 0; }
结果,函数WaitForSingleObject完全没有起到等待的作用.
把CreateMutex(NULL, FALSE, NULL)中的FALSE改成TRUE,结果跟上面一模一样。说好的未触发呢?为什么Wait成功了?
自己给自己的答案:如果创建时第二个参数为TRUE,表示当前互斥量已经加锁,就是主线程拥有该锁,主线程自己用WaitForSingleObject加锁的时候,由于本身已经拥有此锁,所以可以直接向下运行。
如果创建时第二个参数为FALSE,表示当前互斥量未加锁,主线程用WaitForSingleObject加锁的时候就把互斥量加锁了,并且所有权归主线程,同样可以向下运行。
如果之前没有指定互斥量为谁所有,是谁release互斥量就归谁吗? 答:谁加锁,互斥量归谁。
互斥量不能做同步,那么它能做什么呢?答:可以用于互斥
--------------------------------------------------------------
在http://blog.csdn.net/rommi/article/details/6015143中以另一种方式讲述了互斥量,感觉好理解一些
互斥量表现互斥现象的数据结构,也被当作二元信号灯。一个互斥基本上是一个多任务敏感的二元信号,它能用作同步多任务的行为,它常用作保护从中断来的临界段代码并且在共享同步使用的资源。
Mutex本质上说就是一把锁,提供对资源的独占访问,所以Mutex主要的作用是用于互斥。Mutex对象的值,只有0和1两个值。这两个值也分别代表了Mutex的两种状态。值为0, 表示锁定状态,当前对象被锁定,用户进程/线程如果试图Lock临界资源,则进入排队等待;值为1,表示空闲状态,当前对象为空闲,用户进程/线程可以Lock临界资源,之后Mutex值减1变为0。
Mutex可以被抽象为四个操作:
- 创建 Create
- 加锁 Lock
- 解锁 Unlock
- 销毁 Destroy
Mutex被创建时可以有初始值,表示Mutex被创建后,是锁定状态还是空闲状态。在同一个线程中,为了防止死锁,系统不允许连续两次对Mutex加锁(系统一般会在第二次调用立刻返回)。也就是说,加锁和解锁这两个对应的操作,需要在同一个线程中完成。
不同操作系统中提供的Mutex函数:
动作/系统 |
Win32 |
Linyx |
Solaris |
创建 |
CreateMutex |
pthread_mutex_init |
mutex_init |
加锁 |
WaitForSingleObject |
pthread_mutex_lock |
mutex_lock |
解锁 |
ReleaseMutex |
pthread_mutex_unlock |
mutex_unlock |
销毁 |
CloseHandle |
pthread_mutex_destroy |
mutex_destroy |
死锁主要发生在有多个依赖锁存在时, 会在一个线程试图以与另一个线程相反顺序锁住互斥量时发生. 如何避免死锁是使用互斥量应该格外注意的东西。
总体来讲, 有几个不成文的基本原则:
对共享资源操作前一定要获得锁。
完成操作以后一定要释放锁。
尽量短时间地占用锁。
如果有多锁, 如获得顺序是ABC连环扣, 释放顺序也应该是ABC。
线程错误返回时应该释放它所获得的锁。
也许还有读者好奇,“挂起等待”和“唤醒等待线程”的操作如何实现?每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先在把自己加入等待队列中,然后置线程状态为睡眠,然后调用调度器函数切换到别的线程。一个线程要唤醒等待队列中的其它线程,只需从等待队列中取出一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤醒的线程。
一般情况下,如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,这叫做死锁(Deadlock)。另一种典型的死锁情形是这样:线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。不难想象,如果涉及到更多的线程和更多的锁,有没有可能死锁的问题将会变得复杂和难以判断。
-------------------------------
一个互斥量的使用实例http://blog.csdn.net/rommi/article/details/6015200
#include <Windows.h> #include <stdio.h> int tickets = 100; HANDLE hMutex; DWORD WINAPI Thread1Fun(LPVOID lpParam) { while(1) { WaitForSingleObject(hMutex, INFINITE); if(tickets > 0) { printf("t1 = %d\n", tickets--); } else { break; } ReleaseMutex(hMutex); } return 0; } DWORD WINAPI Thread2Fun(LPVOID lpParam) { while (true) { WaitForSingleObject(hMutex,INFINITE); if (tickets>0) { printf("t2 = %d\n", tickets--); } else { break; } ReleaseMutex(hMutex); } return 0; } int main() { HANDLE hThread1, hThread2; hThread1 = CreateThread(NULL, 0, Thread1Fun, NULL, 0, NULL); hThread2 = CreateThread(NULL, 0, Thread2Fun, NULL, 0, NULL); CloseHandle(hThread1); CloseHandle(hThread2); hMutex = CreateMutex(NULL, FALSE, NULL); //TRUE代表主线程拥有互斥对象 但是主线程没有释放该对象 互斥对象谁拥有 谁释放 //FLASE代表当前没有线程拥有这个互斥对象 Sleep(4000); return 0; }
在网上看见一个dll的代码,其中有这样几行
case DLL_PROCESS_ATTACH:
........
hmutex=CreateMutex(NULL,FALSE,NULL);//1
WaitForSingleObject(hmutex,INFINITE);//2
......
ReleaseMutex//3
按照我的理解,程序应该停在2处,因为其他地方没有ReleaseMutex,所以对代码的意义感到很奇怪,后来自己写了一个EXE
hmutex=CreateMutex(NULL,FALSE,NULL);//1
WaitForSingleObject(hmutex,INFINITE);//2
Printf( "result ")//3
结果是很顺利的输出了result,请教原因!谢谢! 20 修改 删除 举报 引用 回复
这个问题第1个回答:
第一,CreateMutex时,第二个参数为FALSE,表示创建者不占有Mutex。这时Mutex的状态是有信号的,所有的wait function将返回。如果第二个参数为TRUE,表示创建者占有了Mutex,这时Mutex的状态是无信号的。其它线程中的wait function将阻塞。
第二,Mutex用于线程之间的同步,在同一线程中执行你上面的语句,不论第二个参数为TRUE或FALSE,wait function都是返回的。
也就是说如果你在第1个线程里执行
hmutex=CreateMutex(NULL,TRUE,NULL);//1
而在另一个线程中执行
WaitForSingleObject(hmutex,INFINITE);//2
如果第一个线程不ReleaseMutex(),第二个线程将一直被阻塞。