標籤:Windows;線程;
線程同步問題
在多線程編程中,極容易產生錯誤。造成錯誤的原因:兩個或多個線程同時訪問了共有的資源(比如全域變數,控制代碼,對空間等),造成資源在不同線程修改時出現不一致。多個線程對於資源的訪問要按照一定的先後順序,但是未按照預想的順序來,就會導致程式出現意想不到的錯誤。
問題執行個體:(環境:vs2015 控制台程式)
#include<Windows.h>#include<stdio.h>int g_nNum = 0;DWORD WINAPI ThreadProc(LPVOID lParam){for (int i = 0; i < 10000; i++){g_nNum++;}printf("%d", g_nNum);return 0;}int main(){//建立線程1HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);//建立線程2HANDLE HThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);WaitForSingleObject(hThread1, INFINITE); //線程1執行完畢後返回WaitForSingleObject(HThread2, INFINITE); //線程2執行完畢後返回printf("%d\n", g_nNum);return 0;}
第一次執行結果:
11789
17876
17876
第二次執行結果:
20000
15844
20000
按照預期,g_nNum在兩個線程中應該各自自增10000,而實際上,g_nNum的值確是不確定的。
首先來看一下自增這個簡單的操縱在彙編層的代碼:
00AE1419 mov eax,dword ptr ds [00AE8134h]00AE141E add eax,100AE1421 mov dword ptr ds:[00AE8134h],eax
兩個線程同時執行g_nNum++這個操作,有可能線程1執行了add eax,還沒有將將自增的結果寫入,線程2又開始執行,當線程1再執行的時候,線程2的執行就相當於已經無用。因為線程的調度是不可控的,所以我們不能預知最後的結果。
解決方案:**
1.原子操作
原子操作是一些比較簡單的操作,只能對資源進行簡單的加減賦值等。當運用原子操作訪問某資料時,其他線程不能在此次操作結束前訪問此資料,即不允許兩個線程同時操作一個資料,當然,也不允許三個。原子操作就像廁所,只允許一個人進入。
常見的原子操作函數自行百度
int g_nNum = 0;DWORD WINAPI ThreadProc(LPVOID lParam){for (int i = 0; i < 10000; i++){//原子操作中的自增,其他的原子操作函數自行百度InterlockedIncrement((unsigned long*)&g_nNum);}printf("%d", g_nNum);return 0;}int main(){HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);HANDLE HThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);WaitForSingleObject(hThread1, INFINITE);WaitForSingleObject(HThread2, INFINITE);printf("%d\n", g_nNum);return 0;}
運行結果
10000
20000
20000
2.臨界區
原子操作僅能夠解決單獨的資料(整型變數的基本運算)的線程同步問題,大多數時候,我們想要實現的是對一個程式碼片段的保護,於是便引入了臨界區這一概念。臨界區通過EnterCriticalSection與LeaveCriticalSection這一對函數,通過這個函數對,就可以實現多個代碼保護區。在使用臨界區前,需要調用InitiaizeCriticalSection初始化一個臨界區,使用完後調用DeleteCriticalSection銷毀臨界區。
#include <windows.h>CRITICAL_SECTION cs = {};int g_nNum = 0;DWORD WINAPI ThreadProc(LPVOID lParam) { // 2. 進入臨界區 // cs有個屬性LockSemaphore是不是被鎖定 // 當調用EnterCriticalSection表示臨界區被鎖定,OwningThread就是該線程 // 其他調用EnterCriticalSection,會檢查和鎖定時的線程是否是同一個線程 // 如果不是,調用Enter的線程就阻塞 // 如果是,就把鎖定計數LockCount+1 // 有幾次Enter就得有幾次Leave // 但是,不是擁有者線程的人不能主動Leave EnterCriticalSection(&cs); for (int i = 0; i < 100000; i++) { g_nNum++; } printf("%d\n", g_nNum); // 3. 離開臨界區 // 萬一,還沒有調用Leave,該線程就崩潰了,或死迴圈了.. // 外面等待的人就永遠等待 // 臨界區不是核心對象, 不能跨進程同步 LeaveCriticalSection(&cs); return 0;}int main(){ // 1. 初始化臨界區 InitializeCriticalSection(&cs); HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL); HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); printf("%d\n", g_nNum); // 4. 銷毀臨界區 DeleteCriticalSection(&cs); return 0;}
3.互斥體
臨界區有很多解決不了的問題,因為臨界區在一個進程中有效,無法在多進程的情況下進行同步。並且,如果一個線程進入到臨界區,結果這個線程由於某些原因奔潰了,即無法執LeaveCriticalSection(),那麼其他線程將無法再進入臨界區,程式奔潰。而互斥體則可以解決這些問題。
首先,互斥體是一個核心對象。(因此互斥體擁有核心對象的一切屬性)它有兩個狀態,激發態和非激發態;它有一個概念叫做線程擁有權,與臨界區類似;等待函數等待互斥體的副作用,將互斥體的擁有者設定為本線程,然後將互斥體的狀態設定為非激發態。
主要函數:CreateMutex();WaitForSingleObject();ReleaseMutex();函數用法自行百度。
當一個線程A調用WaitForSingleObject函數時,WaitForSingleObject會立即返回,將並將互斥體設為非激發態,互斥體被鎖住,此線程獲得擁有權。之後,任何調用WaitForSingleObject的線程無法獲得所有權,必須等待互斥體。當線程A調用ReleaseMutex時,互斥體被解鎖,此時互斥體又被設定為激發態,並會從等待它的線程中隨機選一個,重複前面的過程。互斥體一次只能被一個線程擁有,在WaitXXXX與ReleaseMutex之間的代碼被保護起來,這一點與臨界區類似,只不過互斥體是一個核心對象,可以進行多進程同步。
#include <windows.h>#include<stdio.h>HANDLE hMutex = 0;int g_nNum = 0;// 臨界區和互斥體比較// 1. 互斥體是個核心對象,可以跨進程同步,臨界區不行// 2. 當他們的擁有者線程都崩潰的時候,互斥體可以被系統釋放,變為有訊號,其他的等待函數可以正常返回// 臨界區不行,如果都是假死(死迴圈,無響應),他們都會死結// 3. 臨界區不是核心對象,所以訪問速度比互斥體快DWORD WINAPI ThreadProc(LPVOID lParam) { // 等待某個核心對象,有訊號就返回,無訊號就一直等待 // 返回時把等待的對象變為無訊號狀態 WaitForSingleObject(hMutex, INFINITE); for (int i = 0; i < 100000; i++) { g_nNum++; } printf("%d\n", g_nNum); // 把互斥體變為有訊號狀態 ReleaseMutex(hMutex); return 0;}int main(){ // 1. 建立一個互斥體 hMutex = CreateMutex( NULL, FALSE,// 是否建立時就被當先線程擁有 NULL);// 互斥體名稱 HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL); HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); printf("%d\n", g_nNum); return 0;}
4.訊號量
訊號量與互斥體類似。不過訊號量中引入了訊號數量的概念。如果說互斥體是家裡廁所,在一個時間點只能一個人使用,那訊號量就是公用廁所,可以多個人同時使用,但是仍有上限。這個上限數量即最大訊號數量。
主要函數:CreateSemaphore();OpenSemaphore();ReleaseSemaphore();WaitForSingleObject(); 函數用法自行百度
當有線程調用了WaitForSingleObject();當前訊號量減一,再有線程調用,再減一。為0時,即訊號量被鎖住,再有線程調用WaitForSingleObject時,將被阻塞。
#include <windows.h>#include <stdio.h>HANDLE hSemphore;int g_nNum = 0;DWORD WINAPI ThreadProc(LPVOID lParam) { WaitForSingleObject(hSemphore, INFINITE); for (int i = 0; i < 100000; i++) { g_nNum++; } printf("%d\n", g_nNum); ReleaseSemaphore(hSemphore, 1,// 釋放的訊號個數可以大於1,但是釋放後的訊號個數+之前的不能大於最大值,否則釋放失敗 NULL); return 0;}int main(){ hSemphore = CreateSemaphore( NULL, 1,// 初始訊號個數 1,// 最大訊號個數,就是允許同時訪問保護資源的線程數 NULL); HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL); HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); printf("%d\n", g_nNum); return 0;}
5.事件
事件具有較大的許可權。可以手動設定事件對象為激發態還是非激發態。建立時間對象的時候,可以設定是自動選擇和手動選擇。自動選擇的事件,等待函數返回時,會自動將其狀態設定為非激發態,阻塞其他線程。手動選擇的,事件對象狀態的控制全靠代碼。
主要函數:CreateEventW();OpenEventA();SetEvent();PulseEvent();
CloseEvent();RoseEvent();
#include <windows.h>#include<stdio.h>HANDLE hEvent1, hEvent2;DWORD WINAPI ThreadProcA(LPVOID lParam) { for (int i = 0; i < 10; i++){ WaitForSingleObject(hEvent1, INFINITE); printf("A "); SetEvent(hEvent2); } return 0;}DWORD WINAPI ThreadProcB(LPVOID lParam) { for (int i = 0; i < 10; i++){ WaitForSingleObject(hEvent2, INFINITE); printf("B "); SetEvent(hEvent1); } return 0;}int main(){ // 事件對象,高度自訂的 hEvent1 = CreateEvent( NULL, FALSE,// 自動重設 TRUE,// 有訊號 NULL); // hEvent1自動重設 初始有訊號 任何人通過setevent變為有訊號 resetevent變為無訊號 // hEvent2自動重設 初始無訊號 hEvent2 = CreateEvent(NULL, FALSE, FALSE, NULL); HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProcA, NULL, NULL, NULL); HANDLE hThread2 = CreateThread(NULL, NULL, ThreadProcB, NULL, NULL, NULL); WaitForSingleObject(hThread1, INFINITE); WaitForSingleObject(hThread2, INFINITE); return 0;}
Windows線程同步詳解