首先感谢MoreWindows同学贡献了这么好的多线程学习资料。最近导师要求写多线程程序,但是处处碰壁,于是决定静下心来对多线程进行一番仔细的学习。
本文只是对原文的修改,查看原文请到:http://blog.csdn.net/morewindows/article/details/7470936
为了充分尊重原作者的劳动成果,本文对原文进行了全文引用,并用红色字体表明我自己的学习心得,希望可以帮助到那些和我一样在学习中还存在疑问的小伙伴们。
阅读本篇之前推荐阅读以下姊妹篇:
前面介绍了关键段CS、事件Event在经典线程同步问题中的使用。本篇介绍用互斥量Mutex来解决这个问题。
互斥量也是一个内核对象,它用来确保一个线程独占一个资源的访问。互斥量与关键段的行为非常相似,并且互斥量可以用于不同进程中的线程互斥访问资源。使用互斥量Mutex主要将用到四个函数。下面是这些函数的原型和使用说明。
第一个CreateMutex
函数功能:创建互斥量(注意与事件Event的创建函数对比)
函数原型:
HANDLECreateMutex(
LPSECURITY_ATTRIBUTESlpMutexAttributes,
BOOLbInitialOwner,
LPCTSTRlpName
);
函数说明:
第一个参数表示安全控制,一般直接传入NULL。
第二个参数用来确定互斥量的初始拥有者。如果传入TRUE表示互斥量对象内部会记录创建它的线程的线程ID号并将递归计数设置为1,由于该线程ID非零,所以互斥量处于未触发状态。如果传入FALSE,那么互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这意味互斥量不为任何线程占用,处于触发状态。
第三个参数用来设置互斥量的名称,在多个进程中的线程就是通过名称来确保它们访问的是同一个互斥量。
函数访问值:
成功返回一个表示互斥量的句柄,失败返回NULL。
第二个打开互斥量
函数原型:
HANDLEOpenMutex(
DWORDdwDesiredAccess,
BOOLbInheritHandle,
LPCTSTRlpName //名称
);
函数说明:
第一个参数表示访问权限,对互斥量一般传入MUTEX_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示互斥量句柄继承性,一般传入TRUE即可。
第三个参数表示名称。某一个进程中的线程创建互斥量后,其它进程中的线程就可以通过这个函数来找到这个互斥量。
函数访问值:
成功返回一个表示互斥量的句柄,失败返回NULL。
第三个触发互斥量
函数原型:
BOOLReleaseMutex (HANDLEhMutex)
函数说明:
访问互斥资源前应该要调用等待函数,结束访问时就要调用ReleaseMutex()来表示自己已经结束访问,其它线程可以开始访问了。
最后一个清理互斥量
由于互斥量是内核对象,因此使用CloseHandle()就可以(这一点所有内核对象都一样)。
接下来我们就在经典多线程问题用互斥量来保证主线程与子线程之间的同步,由于互斥量的使用函数类似于事件Event,所以可以仿照上一篇的实现来写出代码:
[cpp]view plaincopyprint?
1. //经典线程同步问题 互斥量Mutex
2. #include <stdio.h>
3. #include <process.h>
4. #include <windows.h>
5.
6. long g_nNum;
7. unsigned int __stdcall Fun(void *pPM);
8. const int THREAD_NUM = 10;
9. //互斥量与关键段
10. HANDLE g_hThreadParameter;
11. CRITICAL_SECTION g_csThreadCode;
12.
13. int main()
14. {
15. printf(" 经典线程同步 互斥量Mutex\n");
16. printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
17.
18. //初始化互斥量与关键段 第二个参数为TRUE表示互斥量为创建线程所有
19. g_hThreadParameter = CreateMutex(NULL, FALSE, NULL);
20. InitializeCriticalSection(&g_csThreadCode);
21.
22. HANDLE handle[THREAD_NUM];
23. g_nNum = 0;
24. int i = 0;
25. while (i < THREAD_NUM)
26. {
27. handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
28. WaitForSingleObject(g_hThreadParameter, INFINITE); //等待互斥量被触发
29. i++;
30. }
31. WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
32.
33. //销毁互斥量和关键段
34. CloseHandle(g_hThreadParameter);
35. DeleteCriticalSection(&g_csThreadCode);
36. for (i = 0; i < THREAD_NUM; i++)
37. CloseHandle(handle[i]);
38. return 0;
39. }
40. unsigned int __stdcall Fun(void *pPM)
41. {
42. int nThreadNum = *(int *)pPM;
43. ReleaseMutex(g_hThreadParameter);//触发互斥量
44.
45. Sleep(50);//some work should to do
46.
47. EnterCriticalSection(&g_csThreadCode);
48. g_nNum++;
49. Sleep(0);//some work should to do
50. printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum);
51. LeaveCriticalSection(&g_csThreadCode);
52. return 0;
53. }
//经典线程同步问题 互斥量Mutex
#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()
{
printf(" 经典线程同步 互斥量Mutex\n");
printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
//初始化互斥量与关键段 第二个参数为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)。
我的一点看法:
(1)新创建的互斥量g_hThreadParameter在主线程建立第一个子线程后被主线程获得,应为线程的拥有权属于主线程,而主线程又没有释放他的权力,导致主线程一直在创建子线程,并且对i进行加操作。在子线程的时间片到达后,由于子线程并不需要获得互斥量g_hThreadParameter的操作权就可以对全局变量g_nNum进行原子的加操作,所以g_nNum得以一直加到10。子线程中的ReleaseMutex(g_hThreadParameter)语句在这里完全没有用,如果测试其返回值,返回的也是false,因为互斥量一直被主线程占有。如果此时在主线程的开始处加上ReleaseMutex(g_hThreadParameter),并且在子线程的开头加上WaitForSingleObject(g_hThreadParameter, INFINITE),则子线程永远得不到互斥量的权限而一直阻塞下去。
(2)对这个程序的改进:
#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()
{
printf(" 经典线程同步互斥量Mutex\n");
printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
g_hThreadParameter = CreateMutex(NULL, FALSE, NULL);
InitializeCriticalSection(&g_csThreadCode);
HANDLE handle[THREAD_NUM];
g_nNum = 0;
int i = 0;
while (i < THREAD_NUM)
{
WaitForSingleObject(g_hThreadParameter, INFINITE);//等待互斥量被触发
handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
Sleep(10);
i++;
ReleaseMutex(g_hThreadParameter);//触发互斥量¢
}
WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
//销毁互斥量和关键段?
CloseHandle(g_hThreadParameter);
DeleteCriticalSection(&g_csThreadCode);
for (i = 0; i < THREAD_NUM; i++)
CloseHandle(handle[i]);
system("pause");
return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
int nThreadNum = *(int *)pPM;
//WaitForSingleObject(g_hThreadParameter, INFINITE);
Sleep(50);//some work should to do
//ReleaseMutex(g_hThreadParameter);
EnterCriticalSection(&g_csThreadCode);
g_nNum++;
Sleep(0);//some work should to do
printf("线程编号为%d 全局资源值为%d\n", nThreadNum, g_nNum);
LeaveCriticalSection(&g_csThreadCode);
return 0;
}
其中红色注释两行对本程序没有实际意义,因为对全局数据的修改在关键段中完成。运行结果:
While循环中的红色Sleep(10)语句在这里有重要作用。如果没有这句,则主线程在不停地创建子线程,在子线程的时间片没有到来之前,i的值一直在增大,这样后续的子线程得到的将会是错误的参数。去除Sleep(10)后的运行结果如下:
加入Sleep(10)的主要作用就在于给子线程传递正确的参数,而不是要实现线程同步的功能;就如同原作者所说,互斥量是不能做到线程的同步的,尤其是在主线程和子线程之间。另外,使用Sleep()函数的时间值较小也会出现类似上面的错误。说明线程的建立时间还是比较苛刻的。
另外对于原作者所说的互斥量所拥有的的“线程拥有权”,我的理解是:这个概念本身听起来就很别扭,其实说的是线程对互斥量或者关键区的占有和访问权力,所以应该说线程对这两者有拥有权,这样就好理解了。由于他们都会记录占有自己访问权限的线程ID,在引用次数减为0后才会撤销该ID。故如果一个线程获得了互斥量,则在它主动放弃对互斥量的拥有权之前,其它线程是不能获得该互斥量的权限的。在别的线程中也无法通过ReleaseMutex()来使其引用次数减为0,因为只有拥有者才有这个权利!所以原作者的程序一中的43行的ReleaseMutex(g_hThreadParameter);//触发互斥量
在执行后返回的将会是一个FALSE值。
下面写二个程序来验证下:
第一个程序创建互斥量并等待用户输入后就触发互斥量。第二个程序先打开互斥量,成功后就等待并根据等待结果作相应的输出。详见代码:
第一个程序:
[cpp]view plaincopyprint?
1. #include <stdio.h>
2. #include <conio.h>
3. #include <windows.h>
4. const char MUTEX_NAME[] = "Mutex_MoreWindows";
5. int main()
6. {
7. HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEX_NAME); //创建互斥量
8. printf("互斥量已经创建,现在按任意键触发互斥量\n");
9. getch();
10. //exit(0);
11. ReleaseMutex(hMutex);
12. printf("互斥量已经触发\n");
13. CloseHandle(hMutex);
14. return 0;
15. }
第二个程序:
[cpp]view plaincopyprint?
1. #include <stdio.h>
2. #include <windows.h>
3. const char MUTEX_NAME[] = "Mutex_MoreWindows";
4. int main()
5. {
6. HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, TRUE, MUTEX_NAME); //打开互斥量
7. if (hMutex == NULL)
8. {
9. printf("打开互斥量失败\n");
10. return 0;
11. }
12. printf("等待中....\n");
13. DWORD dwResult = WaitForSingleObject(hMutex, 20 * 1000); //等待互斥量被触发
14. switch (dwResult)
15. {
16. case WAIT_ABANDONED:
17. printf("拥有互斥量的进程意外终止\n");
18. break;
19.
20. case WAIT_OBJECT_0:
21. printf("已经收到信号\n");
22. break;
23.
24. case WAIT_TIMEOUT:
25. printf("信号未在规定的时间内送到\n");
26. break;
27. }
28. CloseHandle(hMutex);
29. return 0;
30. }
运用这二个程序时要先启动程序一再启动程序二。下面展示部分输出结果:
结果一.二个进程顺利执行完毕:
结果二.将程序一中//exit(0);前面的注释符号去掉,这样程序一在触发互斥量之前就会因为执行exit(0);语句而且退出,程序二会收到WAIT_ABANDONED消息并输出“拥有互斥量的进程意外终止”:
即当占用互斥量的线程意外退出,导致互斥量没有正确被释放,系统也会自动处理这种“遗弃”问题。即使WaitForSingleObject()指定的等待时间是INFINITE也一样,不会存在无穷时间的等待!
有这个对“遗弃”问题的处理,在多进程中的线程同步也可以放心的使用互斥量。
最后总结下互斥量Mutex:
1.互斥量是内核对象,它与关键段都有“线程所有权”所以不能用于线程的同步。
2.互斥量能够用于多个进程之间线程互斥问题,并且能完美的解决某进程意外终止所造成的“遗弃”问题。
下一篇《秒杀多线程第八篇经典线程同步信号量Semaphore》将介绍使用信号量Semaphore来解决这个经典线程同步问题。
本人也是刚刚开始学习多线程,对于文中的纰漏还请各路高手赐教。也希望有更多的朋友可以相互学习,共同进步!