应用多线程互斥锁之前首先简单过一下C程序可能用到的3个创建线程函数:
CreateThread,windows系统提供的唯一创建线程API,_beginthread和_beginthreadex都在内部调用了CreateThread,直接调用该函数创建多线程的C程序存在内存泄露的可能性,通常不推荐直接使用,创建多线程应用程序时以_beginthreadex替代,详细原因下面讲解。
_beginthread,最初版的C运行时库多线程创建函数,参数过少,存在一些天然的缺陷,无法创建具有运行安全属性的线程,无法创建初始状态为挂起的线程,已被_beginthreadex全面替代,更加不推荐使用。
_beginthreadex ,正宗的多线程创建函数,能用_beginthreadex就不要用上面两个,相比CreateThread这个WIN32API,_beginthreadex额外做了线程局部存储空间的分配,C运行时一些函数在使用时会检测线程局部存储结构的指针,如果该指针为空就会自己创建一块内存用作局部存储,但要命的是这些函数并不负责释放这些局部存储空间,而CreateThread并不提供线程局部存储结构的指针,调用这些函数就会导致内存泄露,这些函数包括但不限于malloc、fopen、ctime、localtime。_beginthreadex会准备这样一个内存块,对应的_endthreadex会释放掉这个内存块。
网传很靠谱的_beginthreadex的工作过程:
1.创建C/C++运行期库所需的tiddat结构。
2.传递给_beginthreadex的线程启动函数也保存在tiddat结构中,传递给_beginthreadex的参数也保存在该结构中。
3._beginthreadex调用CreateThread函数创建线程时替换了线程启动函数和参数,线程启动函数替换为_threadstartex,线程启动传入参数替换为tiddat结构。
4.替换后的线程启动函数额外包含了C/C++运行时库所需的局部存储空间信息,同时也保留了CreateThread提供的所有安全属性,创建线程必选它啊。
先展示创建多个线程并使用互斥锁保证线程连续输出屏幕的一段代码,再根据代码讲解注释以外的互斥锁知识:
unsigned int _stdcall ThreadExFunc(void* p)
{
//引用已存在的互斥锁核心对象,成功则核心对象引用计数+1
HANDLE hMutex = OpenMutex(SYNCHRONIZE , TRUE, TEXT("foo46"));
if(hMutex == NULL)
printf("Wrong Mutex");
else
{
//WaitforsingleObject将等待指定的一个mutex,直至获取到拥有权
//通过互斥锁保证除非输出工作全部完成,否则其他线程无法输出。
WaitForSingleObject(hMutex, 1000);
for(int i = 0; i < 10; i++)
{
printf("%d%d%d%d%d%d%d%d%d%d\n", p, p, p, p, p, p, p, p, p,p);
//休息10ms,大约为CPU的一个任务切换分片,即切换到下一个线程工作。
Sleep(10);
}
ReleaseMutex(hMutex);
}
//引用计数-1
CloseHandle(hMutex);
return 0;
}
void foo46()
{
int i;
unsigned int hTrd[5];
unsigned int iThreadID;
BOOL bRc;
DWORD ExitCode;
BOOL bRunning = FALSE;
//CreateMutex后核心对象hMutex引用计数为1,当有其他线程OpenMutex或者CreateMutex时引用计数+1
//MUTEX的拥有权属于最后一个Wait并且尚未Release的线程
//MUTEX的摧毁和拥有权没有关系,当有一个线程对此调用CloseHandle时引用计数减1,引用计数为0时被摧毁。
HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("foo46"));
for(i = 0; i < 5; i++)
{
//创建一个线程后,调用_beginthreadex会创建该线程核心对象,被创建的线程也会开启该核心对象
//线程创建后该核心对象引用计数为2
hTrd[i] = _beginthreadex(NULL,
0,
ThreadExFunc,
(void*)i,
0,
&iThreadID);
//创建线程后即可调用CloseHandle关闭线程核心对象,调用CloseHandle只不过表示自己不想和这个线程对象有任何关系,调用后也没有真正的关闭
//线程核心对象,只是将线程核心对象的引用计数减一,如果引用计数变为0,该核心对象会被摧毁。
//线程核心对象并不指向线程本身
}
//等待所有工作线程都推出后才退出主线程
//进程启动后的第一个线程叫做主线程,主线程的结束会导致其他所有线程强制退出
//在窗口程序中主线程默认处理消息队列,通常将主线程设置为界面线程。
while(bRunning)
{
bRunning = FALSE;
for(int i = 0; i < 5; i++)
{
bRc = GetExitCodeThread((HANDLE)hTrd[i], &ExitCode);
if(bRc && ExitCode == STILL_ACTIVE)
bRunning = TRUE;
}
}
for(int i = 0; i < 5; i++)
{
CloseHandle((HANDLE)hTrd[i]);
}
}
互斥锁的使用通常有以下API:
CreateMutex,创建一个互斥锁核心对象,如果创建的核心对象已存在,会返回句柄,如果已经存在调用GetLastError会返回ERROR_ALREADY_EXITS。
OpenMutex,打开一个已经存在互斥锁核心对象,通常用于客户端获取服务器的互斥锁对象。
WaitForSingleObject,尝试获取一个内核核心对象的拥有权,可以设置等待时间,有三种情况会返回:1.成功获取拥有权;2.等待超时返回;3.等待中的互斥锁对象被废弃,即线程Wait到MUTEX后尚未调用ReleaseMutex就异常退出,通过WaitForSingleObject的返回值可以判断。
WaitForMultiPleObjects,尝试获取多个内核核心对象的拥有权,可以设置互斥锁对象列表、是否等待所有、等待超时时间。使用和WaitForSingleObject不是很一样,详细参见MSDN。
MsgWaitForMultipleObjects,WaitForMultipleObjects只有在对象被激发或核心对象废弃再或者等待超时才会返回,而主界面窗口还要等待消息,如果有消息到来也要立即返回并处理消息,MsgWaitForMultipleObjects就是这样一个同时等待消息和对象激发信号的函数。
ReleaseMutex,工作完成后调用,释放互斥锁拥有权。
CloseHandle,告诉操作系统不再需要引用该内核对象,请将引用计数-1.
小结:互斥锁的基础知识大概就这么多了,应用时要麻烦的多,就如侯捷所说:知道哪一个mutex被舍弃是一件简单的事,想要正确处理却不容易,mutex是用来保护操作自动进行的,如果线程死于半途,很有可能被保护的数据就会受到不可修复的伤害。