注:所謂同步,並不是多個線程一起同時執行,而是他們協同步調,按預定的先后次序執行。
與線程相關的基本函數包括:
CreateThread:創建線程
CloseHandle:關閉線程句柄。注意,這只會使指定的線程句柄無效(減少該句柄的引用計數),啟動句柄的檢查操作,如果一個對象所關聯的最后一個句柄被關閉了,那么這個對象會從系統中被刪除。關閉句柄不會終止相關的線程。
線程是如何運行的呢?這又與你的CPU有關系了,如果你是一個單核CPU,那么系統會采用時間片輪詢的方式運行每個線程;如果你是多核CPU,那么線程之間就有可能並發運行了。這樣就會出現很多問題,比如兩個線程同時訪問一個全局變量之類的。它們需要線程的同步來解決。所謂同步,並不是多個線程一起同時執行,而是他們協同步調,按預定的先后次序執行。
Windows下線程同步的基本方法有3種:互斥對象、事件對象、關鍵代碼段(臨界區),下面一一介紹:
互斥對象屬於內核對象,包含3個成員:
1.使用數量:記錄了有多少個線程在調用該對象
2.一個線程ID:記錄互斥對象維護的線程的ID
3.一個計數器:前線程調用該對象的次數
與之相關的函數包括:
創建互斥對象:CreateMutex
判斷能否獲得互斥對象:WaitForSingleObject
對於WaitForSingleObject,如果互斥對象為有信號狀態,則獲取成功,函數將互斥對象設置為無信號狀態,程序將繼續往下執行;如果互斥對象為無信號狀態,則獲取失敗,線程會停留在這里等待。等待的時間可以由參數控制。
釋放互斥對象:ReleaseMutex
當要保護的代碼執行完畢后,通過它來釋放互斥對象,使得互斥對象變為有信號狀態,以便於其他線程可以獲取這個互斥對象。注意,只有當某個線程擁有互斥對象時,才能夠釋放互斥對象,在其他線程調用這個函數不得達到釋放的效果,這可以通過互斥對象的線程ID來判斷。
1 #include <Windows.h> 2 #include <stdio.h> 3 4 //線程函數聲明 5 DWORD WINAPI Thread1Proc( LPVOID lpParameter); 6 DWORD WINAPI Thread2Proc( LPVOID lpParameter); 7 8 //全局變量 9 int tickets = 100; 10 HANDLE hMutex; 11 12 int main() 13 { 14 HANDLE hThread1; 15 HANDLE hThread2; 16 //創建互斥對象 17 hMutex = CreateMutex( NULL, //默認安全級別 18 FALSE, //創建它的線程不擁有互斥對象 19 NULL); //沒有名字 20 //創建線程1 21 hThread1 = CreateThread(NULL, //默認安全級別 22 0, //默認棧大小 23 Thread1Proc,//線程函數 24 NULL, //函數沒有參數 25 0, //創建后直接運行 26 NULL); //線程標識,不需要 27 28 //創建線程2 29 hThread2 = CreateThread(NULL, //默認安全級別 30 0, //默認棧大小 31 Thread2Proc,//線程函數 32 NULL, //函數沒有參數 33 0, //創建后直接運行 34 NULL); //線程標識,不需要 35 36 //主線程休眠4秒 37 Sleep(4000); 38 //主線程休眠4秒 39 Sleep(4000); 40 //關閉線程句柄 41 CloseHandle(hThread1); 42 CloseHandle(hThread2); 43 44 //釋放互斥對象 45 ReleaseMutex(hMutex); 46 return 0; 47 } 48 49 //線程1入口函數 50 DWORD WINAPI Thread1Proc( LPVOID lpParameter) 51 { 52 while(TRUE) 53 { 54 WaitForSingleObject(hMutex,INFINITE); 55 if(tickets > 0) 56 { 57 Sleep(10); 58 printf("thread1 sell ticket : %d\n",tickets--); 59 ReleaseMutex(hMutex); 60 } 61 else 62 { 63 ReleaseMutex(hMutex); 64 break; 65 } 66 } 67 68 return 0; 69 } 70 71 //線程2入口函數 72 DWORD WINAPI Thread2Proc( LPVOID lpParameter) 73 { 74 while(TRUE) 75 { 76 WaitForSingleObject(hMutex,INFINITE); 77 if(tickets > 0) 78 { 79 Sleep(10); 80 printf("thread2 sell ticket : %d\n",tickets--); 81 ReleaseMutex(hMutex); 82 } 83 else 84 { 85 ReleaseMutex(hMutex); 86 break; 87 } 88 } 89 90 return 0; 91 }
1 使用互斥對象時需要小心: 2 調用假如一個線程本身已經擁有該互斥對象,則如果它繼續調用WaitForSingleObject,則會增加互斥對象的引用計數,此時,你必須多次調用ReleaseMutex來釋放互斥對象,以便讓其他線程可以獲取:
1 //創建互斥對象 2 hMutex = CreateMutex( NULL, //默認安全級別 3 TRUE, //創建它的線程擁有互斥對象 4 NULL); //沒有名字 5 WaitForSingleObject(hMutex,INFINITE); 6 //釋放互斥對象 7 ReleaseMutex(hMutex); 8 //釋放互斥對象 9 ReleaseMutex(hMutex);
下面看事件對象,它也屬於內核對象,包含3各成員:
1.使用計數
2.用於指明該事件是自動重置事件還是人工重置事件的布爾值
3.用於指明該事件處於已通知狀態還是未通知狀態。
自動重置和人工重置的事件對象有一個重要的區別:當人工重置的事件對象得到通知時,等待該事件對象的所有線程均變為可調度線程;而當一個自動重置的事件對象得到通知時,等待該事件對象的線程中只有一個線程變為可調度線程。
與事件對象相關的函數包括:
創建事件對象:CreateEvent
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState,LPCTSTR lpName);
設置事件對象:SetEvent:將一個這件對象設為有信號狀態
BOOL SetEvent( HANDLE hEvent );
重置事件對象狀態:ResetEvent:將指定的事件對象設為無信號狀態
BOOL ResetEvent( HANDLE hEvent );
下面仍然使用買火車票的例子:
1 #include <Windows.h> 2 #include <stdio.h> 3 4 //線程函數聲明 5 DWORD WINAPI Thread1Proc( LPVOID lpParameter); 6 DWORD WINAPI Thread2Proc( LPVOID lpParameter); 7 8 //全局變量 9 int tickets = 100; 10 HANDLE g_hEvent; 11 12 int main() 13 { 14 HANDLE hThread1; 15 HANDLE hThread2; 16 //創建事件對象 17 g_hEvent = CreateEvent( NULL, //默認安全級別 18 TRUE, //人工重置 19 FALSE, //初始為無信號 20 NULL ); //沒有名字 21 //創建線程1 22 hThread1 = CreateThread(NULL, //默認安全級別 23 0, //默認棧大小 24 Thread1Proc,//線程函數 25 NULL, //函數沒有參數 26 0, //創建后直接運行 27 NULL); //線程標識,不需要 28 29 //創建線程2 30 hThread2 = CreateThread(NULL, //默認安全級別 31 0, //默認棧大小 32 Thread2Proc,//線程函數 33 NULL, //函數沒有參數 34 0, //創建后直接運行 35 NULL); //線程標識,不需要 36 37 38 //主線程休眠4秒 39 Sleep(4000); 40 //關閉線程句柄 41 //當不再引用這個句柄時,立即將其關閉,減少其引用計數 42 CloseHandle(hThread1); 43 CloseHandle(hThread2); 44 //關閉事件對象句柄 45 CloseHandle(g_hEvent); 46 return 0; 47 } 48 49 //線程1入口函數 50 DWORD WINAPI Thread1Proc( LPVOID lpParameter) 51 { 52 while(TRUE) 53 { 54 WaitForSingleObject(g_hEvent,INFINITE); 55 if(tickets > 0) 56 { 57 Sleep(1); 58 printf("thread1 sell ticket : %d\n",tickets--); 59 } 60 else 61 break; 62 } 63 64 return 0; 65 } 66 67 //線程2入口函數 68 DWORD WINAPI Thread2Proc( LPVOID lpParameter) 69 { 70 while(TRUE) 71 { 72 WaitForSingleObject(g_hEvent,INFINITE); 73 if(tickets > 0) 74 { 75 Sleep(1); 76 printf("thread2 sell ticket : %d\n",tickets--); 77 } 78 else 79 break; 80 } 81 82 return 0; 83 }
程序運行后並沒有出現兩個線程買票的情況,而是等待了4秒之后直接退出了,這是什么原因呢?問題出在了我們創建的事件對象一開始就是無信號狀態的,因此2個線程線程運行到WaitForSingleObject時就會一直等待,直到自己的時間片結束。所以什么也不會輸出。
如果想讓線程能夠執行,可以在創建線程時將第3個參數設為TRUE,或者在創建完成后調用
1 SetEvent(g_hEvent);
程序的確可以實現買票了,但是有些時候,會打印出某個線程賣出第0張票的情況,這是因為當人工重置的事件對象得到通知時,等待該對象的所有線程均可變為可調度線程,兩個線程同時運行,線程的同步失敗了。
也許有人會想到,在線程得到CPU之后,能否使用ResetEvent是得線程將事件對象設為無信號狀態,然后當所保護的代碼運行完成后,再將事件對象設為有信號狀態?我們可以試試:
1 //線程1入口函數 2 DWORD WINAPI Thread1Proc( LPVOID lpParameter) 3 { 4 while(TRUE) 5 { 6 WaitForSingleObject(g_hEvent,INFINITE); 7 ResetEvent(g_hEvent); 8 if(tickets > 0) 9 { 10 Sleep(10); 11 printf("thread1 sell ticket : %d\n",tickets--); 12 SetEvent(g_hEvent); 13 } 14 else 15 { 16 SetEvent(g_hEvent); 17 break; 18 } 19 } 20 21 return 0; 22 }
線程2的類似,這里就省略了。運行程序,發現依然會出現賣出第0張票的情況。這是為什么呢?我們仔細思考一下:單核CPU下,可能線程1執行完WaitForSingleObject,還沒來得及執行ResetEvent時,就切換到線程2了,此時,由於線程1並沒有執行ResetEvent,所以線程2也可以得到事件對象了。而在多CPU平台下,假如兩個線程同時執行,則有可能都執行到本應被保護的代碼區域。
所以,為了實現線程間的同步,不應該使用人工重置的事件對象,而應該使用自動重置的事件對象:
1 hThread2 = CreateThread(NULL,0,Thread2Proc,NULL0,NULL);
並將原來寫的ResetEvent和SetEvent全都注釋起來。我們發現程序只打印了一次買票過程。我們分析一下原因:
當一個自動重置的事件得到通知后,等待該該事件的線程中只有一個變為可調度線程。在這里,線程1變為可調度線程后,操作系統將事件設為了無信號狀態,當線程1休眠時,所以線程2只能等待,時間片結束以后,又輪到線程1運行,輸出thread1 sell ticket :100。然后循環,又去WaitForSingleObject,而此時事件對象處於無信號狀態,所以線程不能繼續往下執行,只能一直等待,等到自己時間片結束,直到主線程睡醒了,結束整個程序。
正確的使用方法是:當訪問完對保護的代碼段后,立即調用SetEvent將其設為有信號狀態。允許其他等待該對象的線程變為可調度狀態:
1 DWORD WINAPI Thread1Proc( LPVOID lpParameter) 2 { 3 while(TRUE) 4 { 5 WaitForSingleObject(g_hEvent,INFINITE); 6 if(tickets > 0) 7 { 8 Sleep(10); 9 printf("thread1 sell ticket : %d\n",tickets--); 10 SetEvent(g_hEvent); 11 } 12 else 13 { 14 SetEvent(g_hEvent); 15 break; 16 } 17 } 18 19 return 0; 20 }
總結一下:事件對象要區分人工重置事件還是自動重置事件。如果是人工重置的事件對象得到通知,則等待該事件對象的所有線程均變為可調度線程;當一個自動重置的事件對象得到通知時,只有一個等待該事件對象的線程變為可調度線程,且操作系統會將該事件對象設為無信號狀態。因此,當執行完受保護的代碼后,需要調用SetEvent將事件對象設為有信號狀態。
下面介紹另一種線程同步的方法:關鍵代碼段。
關鍵代碼段又稱為臨界區,工作在用戶方式下。它是一小段代碼,但是在代碼執行之前,必須獨占某些資源的訪問權限。
我們先介紹與之先關的API函數:
使用InitializeCriticalSection初始化關鍵代碼段
使用EnterCriticalSection進入關鍵代碼段:
使用LeaveCriticalSection離開關鍵代碼段:
使用DeleteCriticalSection刪除關鍵代碼段,釋放資源
我們看一個例子:
1 #include <Windows.h> 2 #include <stdio.h> 3 4 //線程函數聲明 5 DWORD WINAPI Thread1Proc( LPVOID lpParameter); 6 DWORD WINAPI Thread2Proc( LPVOID lpParameter); 7 8 //全局變量 9 int tickets = 100; 10 CRITICAL_SECTION g_cs; 11 12 int main() 13 { 14 HANDLE hThread1; 15 HANDLE hThread2; 16 //初始化關鍵代碼段 17 InitializeCriticalSection(&g_cs); 18 //創建線程1 19 hThread1 = CreateThread(NULL, //默認安全級別 20 0, //默認棧大小 21 Thread1Proc,//線程函數 22 NULL, //函數沒有參數 23 0, //創建后直接運行 24 NULL); //線程標識,不需要 25 26 //創建線程2 27 hThread2 = CreateThread(NULL, //默認安全級別 28 0, //默認棧大小 29 Thread2Proc,//線程函數 30 NULL, //函數沒有參數 31 0, //創建后直接運行 32 NULL); //線程標識,不需要 33 34 35 //主線程休眠4秒 36 Sleep(4000); 37 //關閉線程句柄 38 CloseHandle(hThread1); 39 CloseHandle(hThread2); 40 //關閉事件對象句柄 41 DeleteCriticalSection(&g_cs); 42 return 0; 43 } 44 45 //線程1入口函數 46 DWORD WINAPI Thread1Proc( LPVOID lpParameter) 47 { 48 while(TRUE) 49 { 50 //進入關鍵代碼段前調用該函數判斷否能得到臨界區的使用權 51 EnterCriticalSection(&g_cs); 52 Sleep(1); 53 if(tickets > 0) 54 { 55 Sleep(1); 56 printf("thread1 sell ticket : %d\n",tickets--); 57 //訪問結束后釋放臨界區對象的使用權 58 LeaveCriticalSection(&g_cs); 59 Sleep(1); 60 } 61 else 62 { 63 LeaveCriticalSection(&g_cs); 64 break; 65 } 66 } 67 68 return 0; 69 } 70 71 //線程2入口函數 72 DWORD WINAPI Thread2Proc( LPVOID lpParameter) 73 { 74 while(TRUE) 75 { 76 //進入關鍵代碼段前調用該函數判斷否能得到臨界區的使用權 77 EnterCriticalSection(&g_cs); 78 Sleep(1); 79 if(tickets > 0) 80 { 81 Sleep(1); 82 printf("thread2 sell ticket : %d\n",tickets--); 83 //訪問結束后釋放臨界區對象的使用權 84 LeaveCriticalSection(&g_cs); 85 Sleep(1); 86 } 87 else 88 { 89 LeaveCriticalSection(&g_cs); 90 break; 91 } 92 } 93 94 return 0; 95 }
在這個例子中,通過在放棄臨界區資源后,立即睡眠引起另一個線程被調用,導致兩個線程交替售票。
下面看一個多線程程序中常犯的一個錯誤-線程死鎖。死鎖產生的原因,舉個例子:線程1擁有臨界區資源A,正在等待臨界區資源B;而線程2擁有臨界區資源B,正在等待臨界區資源A。它倆各不相讓,結果誰也執行不了。我們看看程序:
1 #include <Windows.h> 2 #include <stdio.h> 3 4 //線程函數聲明 5 DWORD WINAPI Thread1Proc( LPVOID lpParameter); 6 DWORD WINAPI Thread2Proc( LPVOID lpParameter); 7 8 //全局變量 9 int tickets = 100; 10 CRITICAL_SECTION g_csA; 11 CRITICAL_SECTION g_csB; 12 int main() 13 { 14 HANDLE hThread1; 15 HANDLE hThread2; 16 //初始化關鍵代碼段 17 InitializeCriticalSection(&g_csA); 18 InitializeCriticalSection(&g_csB); 19 //創建線程1 20 hThread1 = CreateThread(NULL, //默認安全級別 21 0, //默認棧大小 22 Thread1Proc,//線程函數 23 NULL, //函數沒有參數 24 0, //創建后直接運行 25 NULL); //線程標識,不需要 26 27 //創建線程2 28 hThread2 = CreateThread(NULL, //默認安全級別 29 0, //默認棧大小 30 Thread2Proc,//線程函數 31 NULL, //函數沒有參數 32 0, //創建后直接運行 33 NULL); //線程標識,不需要 34 //關閉線程句柄 35 //當不再引用這個句柄時,立即將其關閉,減少其引用計數 36 CloseHandle(hThread1); 37 CloseHandle(hThread2); 38 39 //主線程休眠4秒 40 Sleep(4000); 41 42 //關閉事件對象句柄 43 DeleteCriticalSection(&g_csA); 44 DeleteCriticalSection(&g_csB); 45 return 0; 46 } 47 48 //線程1入口函數 49 DWORD WINAPI Thread1Proc( LPVOID lpParameter) 50 { 51 while(TRUE) 52 { 53 EnterCriticalSection(&g_csA); 54 Sleep(1); 55 EnterCriticalSection(&g_csB); 56 if(tickets > 0) 57 { 58 Sleep(1); 59 printf("thread1 sell ticket : %d\n",tickets--); 60 LeaveCriticalSection(&g_csB); 61 LeaveCriticalSection(&g_csA); 62 Sleep(1); 63 } 64 else 65 { 66 LeaveCriticalSection(&g_csB); 67 LeaveCriticalSection(&g_csA); 68 break; 69 } 70 } 71 72 return 0; 73 } 74 75 //線程2入口函數 76 DWORD WINAPI Thread2Proc( LPVOID lpParameter) 77 { 78 while(TRUE) 79 { 80 EnterCriticalSection(&g_csB); 81 Sleep(1); 82 EnterCriticalSection(&g_csA); 83 if(tickets > 0) 84 { 85 Sleep(1); 86 printf("thread2 sell ticket : %d\n",tickets--); 87 LeaveCriticalSection(&g_csA); 88 LeaveCriticalSection(&g_csB); 89 Sleep(1); 90 } 91 else 92 { 93 LeaveCriticalSection(&g_csA); 94 LeaveCriticalSection(&g_csB); 95 break; 96 } 97 } 98 99 return 0; 100 }
在程序中,創建了兩個臨界區對象g_csA和g_csB。線程1中先嘗試獲取g_csA,獲取成功后休眠,線程2嘗試獲取g_csB,成功后休眠,切換回線程1,然后線程1試圖獲取g_csB,因為g_csB已經被線程2獲取,所以它線程1的獲取不會成功,一直等待,直到自己的時間片結束后,轉到線程2,線程2獲取g_csB后,試圖獲取g_csA,當然也不會成功,轉回線程1……這樣交替等待,直到主線程睡醒,執行完畢,程序結束。