1. 程式人生 > >Windows線程同步詳解

Windows線程同步詳解

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()
{
//創建線程1
HANDLE hThread1 = CreateThread(NULL, NULL, ThreadProc, NULL, NULL, NULL);
//創建線程2
HANDLE 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,1
00AE1421 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線程同步詳解