VC++ 線程同步 總結


注:所謂同步,並不是多個線程一起同時執行,而是他們協同步調,按預定的先后次序執行。

 

與線程相關的基本函數包括:
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……這樣交替等待,直到主線程睡醒,執行完畢,程序結束。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM