Windows線程同步詳解

來源:互聯網
上載者:User

標籤: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線程同步詳解

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.