應用多線程互斥鎖之前首先簡單過一下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是用來保護操作自動進行的,如果線程死於半途,很有可能被保護的數據就會受到不可修復的傷害。