1. 程式人生 > >C++執行緒同步的四種方式(Windows)

C++執行緒同步的四種方式(Windows)

為什麼要進行執行緒同步?

  在程式中使用多執行緒時,一般很少有多個執行緒能在其生命期內進行完全獨立的操作。更多的情況是一些執行緒進行某些處理操作,而其他的執行緒必須對其處理結果進行了解。正常情況下對這種處理結果的瞭解應當在其處理任務完成後進行。
  如果不採取適當的措施,其他執行緒往往會線上程處理任務結束前就去訪問處理結果,這就很有可能得到有關處理結果的錯誤瞭解。例如,多個執行緒同時訪問同一個全域性變數,如果都是讀取操作,則不會出現問題。如果一個執行緒負責改變此變數的值,而其他執行緒負責同時讀取變數內容,則不能保證讀取到的資料是經過寫執行緒修改後的。
  為了確保讀執行緒讀取到的是經過修改的變數,就必須在向變數寫入資料時禁止其他執行緒對其的任何訪問,直至賦值過程結束後再解除對其他執行緒的訪問限制。這種保證執行緒能瞭解其他執行緒任務處理結束後的處理結果而採取的保護措施即為執行緒同步。

程式碼示例:
兩個執行緒同時對一個全域性變數進行加操作,演示了多執行緒資源訪問衝突的情況。

#include "stdafx.h"
#include<windows.h>
#include<iostream>
using namespace std;

int number = 1;

unsigned long __stdcall ThreadProc1(void* lp)
{
    while (number < 100)
    {
        cout << "thread 1 :"<<number << endl;
        ++number;
        _sleep(100
); } return 0; } unsigned long __stdcall ThreadProc2(void* lp) { while (number < 100) { cout << "thread 2 :"<<number << endl; ++number; _sleep(100); } return 0; } int main() { CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL); CreateThread(NULL, 0
, ThreadProc2, NULL, 0, NULL); Sleep(10*1000); system("pause"); return 0; }

執行結果:
這裡寫圖片描述
可以看到有時兩個執行緒計算的值相同,不是我們想要的結果。

關於執行緒同步

執行緒之間通訊的兩個基本問題是互斥和同步。

  • 執行緒同步是指執行緒之間所具有的一種制約關係,一個執行緒的執行依賴另一個執行緒的訊息,當它沒有得到另一個執行緒的訊息時應等待,直到訊息到達時才被喚醒。
  • 執行緒互斥是指對於共享的作業系統資源(指的是廣義的”資源”,而不是Windows的.res檔案,譬如全域性變數就是一種共享資源),在各執行緒訪問時的排它性。當有若干個執行緒都要使用某一共享資源時,任何時刻最多隻允許一個執行緒去使用,其它要使用該資源的執行緒必須等待,直到佔用資源者釋放該資源。

執行緒互斥是一種特殊的執行緒同步。實際上,互斥和同步對應著執行緒間通訊發生的兩種情況:

  • 當有多個執行緒訪問共享資源而不使資源被破壞時;
  • 當一個執行緒需要將某個任務已經完成的情況通知另外一個或多個執行緒時。

從大的方面講,執行緒的同步可分使用者模式的執行緒同步和核心物件的執行緒同步兩大類。

  • 使用者模式中執行緒的同步方法主要有原子訪問和臨界區等方法。其特點是同步速度特別快,適合於對執行緒執行速度有嚴格要求的場合。
  • 核心物件的執行緒同步則主要由事件、等待定時器、訊號量以及訊號燈等核心物件構成。由於這種同步機制使用了核心物件,使用時必須將執行緒從使用者模式切換到核心模式,而這種轉換一般要耗費近千個CPU週期,因此同步速度較慢,但在適用性上卻要遠優於使用者模式的執行緒同步方式。

在WIN32中,同步機制主要有以下幾種:
(1)事件(Event);
(2)訊號量(semaphore);
(3)互斥量(mutex);
(4)臨界區(Critical section)。

Win32中的四種同步方式

臨界區

臨界區(Critical Section)是一段獨佔對某些共享資源訪問的程式碼,在任意時刻只允許一個執行緒對共享資源進行訪問。如果有多個執行緒試圖同時訪問臨界區,那麼在有一個執行緒進入後其他所有試圖訪問此臨界區的執行緒將被掛起,並一直持續到進入臨界區的執行緒離開。臨界區在被釋放後,其他執行緒可以繼續搶佔,並以此達到用原子方式操作共享資源的目的。

臨界區在使用時以CRITICAL_SECTION結構物件保護共享資源,並分別用EnterCriticalSection()和LeaveCriticalSection()函式去標識和釋放一個臨界區。所用到的CRITICAL_SECTION結構物件必須經過InitializeCriticalSection()的初始化後才能使用,而且必須確保所有執行緒中的任何試圖訪問此共享資源的程式碼都處在此臨界區的保護之下。否則臨界區將不會起到應有的作用,共享資源依然有被破壞的可能。

程式碼示例:

#include "stdafx.h"
#include<windows.h>
#include<iostream>
using namespace std;

int number = 1; //定義全域性變數
CRITICAL_SECTION Critical;      //定義臨界區控制代碼

unsigned long __stdcall ThreadProc1(void* lp)
{
    while (number < 100)
    {
        EnterCriticalSection(&Critical);
        cout << "thread 1 :"<<number << endl;
        ++number;
        _sleep(100);
        LeaveCriticalSection(&Critical);
    }

    return 0;
}

unsigned long __stdcall ThreadProc2(void* lp)
{
    while (number < 100)
    {
        EnterCriticalSection(&Critical);
        cout << "thread 2 :"<<number << endl;
        ++number;
        _sleep(100);
        LeaveCriticalSection(&Critical);
    }

    return 0;
}

int main()
{
    InitializeCriticalSection(&Critical);   //初始化臨界區物件

    CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
    CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);

    Sleep(10*1000);

    system("pause");
    return 0;
}

執行結果:
這裡寫圖片描述
可以看到,也實現了有序輸出,實現了執行緒同步。

事件

事件(Event)是WIN32提供的最靈活的執行緒間同步方式,事件可以處於激發狀態(signaled or true)或未激發狀態(unsignal or false)。根據狀態變遷方式的不同,事件可分為兩類:
(1)手動設定:這種物件只可能用程式手動設定,在需要該事件或者事件發生時,採用SetEvent及ResetEvent來進行設定。
(2)自動恢復:一旦事件發生並被處理後,自動恢復到沒有事件狀態,不需要再次設定。

使用”事件”機制應注意以下事項:
(1)如果跨程序訪問事件,必須對事件命名,在對事件命名的時候,要注意不要與系統名稱空間中的其它全域性命名物件衝突;
(2)事件是否要自動恢復;
(3)事件的初始狀態設定。

由於event物件屬於核心物件,故程序B可以呼叫OpenEvent函式通過物件的名字獲得程序A中event物件的控制代碼,然後將這個控制代碼用於ResetEvent、SetEvent和WaitForMultipleObjects等函式中。此法可以實現一個程序的執行緒控制另一程序中執行緒的執行,例如:

HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent"); 
ResetEvent(hEvent);

程式碼示例:

#include "stdafx.h"
#include<windows.h>
#include<iostream>
using namespace std;

int number = 1; //定義全域性變數
HANDLE hEvent;  //定義事件控制代碼

unsigned long __stdcall ThreadProc1(void* lp)
{
    while (number < 100)
    {
        WaitForSingleObject(hEvent, INFINITE);  //等待物件為有訊號狀態
        cout << "thread 1 :"<<number << endl;
        ++number;
        _sleep(100);
        SetEvent(hEvent);
    }

    return 0;
}

unsigned long __stdcall ThreadProc2(void* lp)
{
    while (number < 100)
    {
        WaitForSingleObject(hEvent, INFINITE);  //等待物件為有訊號狀態
        cout << "thread 2 :"<<number << endl;
        ++number;
        _sleep(100);
        SetEvent(hEvent);
    }

    return 0;
}

int main()
{
    CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
    CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);
    hEvent = CreateEvent(NULL, FALSE, TRUE, "event");

    Sleep(10*1000);

    system("pause");
    return 0;
}

執行結果:
這裡寫圖片描述
可以看到,實現了有序輸出,實現了執行緒同步。

訊號量

訊號量是維護0到指定最大值之間的同步物件。訊號量狀態在其計數大於0時是有訊號的,而其計數是0時是無訊號的。訊號量物件在控制上可以支援有限數量共享資源的訪問。

訊號量的特點和用途可用下列幾句話定義:
(1)如果當前資源的數量大於0,則訊號量有效;
(2)如果當前資源數量是0,則訊號量無效;
(3)系統決不允許當前資源的數量為負值;
(4)當前資源數量決不能大於最大資源數量。

建立訊號量

函式原型為:

 HANDLE CreateSemaphore (
   PSECURITY_ATTRIBUTE psa, //訊號量的安全屬性
   LONG lInitialCount, //開始時可供使用的資源數
   LONG lMaximumCount, //最大資源數
   PCTSTR pszName);     //訊號量的名稱

釋放訊號量

通過呼叫ReleaseSemaphore函式,執行緒就能夠對信標的當前資源數量進行遞增,該函式原型為:

BOOL WINAPI ReleaseSemaphore(
   HANDLE hSemaphore,   //要增加的訊號量控制代碼
   LONG lReleaseCount, //訊號量的當前資源數增加lReleaseCount
   LPLONG lpPreviousCount  //增加前的數值返回
   );

開啟訊號量 

和其他核心物件一樣,訊號量也可以通過名字跨程序訪問,開啟訊號量的API為:

 HANDLE OpenSemaphore (
   DWORD fdwAccess,      //access
   BOOL bInherithandle,  //如果允許子程序繼承控制代碼,則設為TRUE
   PCTSTR pszName  //指定要開啟的物件的名字
  );

程式碼示例:

#include "stdafx.h"
#include<windows.h>
#include<iostream>
using namespace std;

int number = 1; //定義全域性變數
HANDLE hSemaphore;  //定義訊號量控制代碼

unsigned long __stdcall ThreadProc1(void* lp)
{
    long count;
    while (number < 100)
    {
        WaitForSingleObject(hSemaphore, INFINITE);  //等待訊號量為有訊號狀態
        cout << "thread 1 :"<<number << endl;
        ++number;
        _sleep(100);
        ReleaseSemaphore(hSemaphore, 1, &count);
    }

    return 0;
}

unsigned long __stdcall ThreadProc2(void* lp)
{
    long count;
    while (number < 100)
    {
        WaitForSingleObject(hSemaphore, INFINITE);  //等待訊號量為有訊號狀態
        cout << "thread 2 :"<<number << endl;
        ++number;
        _sleep(100);
        ReleaseSemaphore(hSemaphore, 1, &count);
    }

    return 0;
}

int main()
{
    hSemaphore = CreateSemaphore(NULL, 1, 100, "sema");

    CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
    CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);

    Sleep(10*1000);

    system("pause");
    return 0;
}

執行結果:
這裡寫圖片描述
可以看到,實現了有序輸出,實現了執行緒間同步。

互斥量

採用互斥物件機制。 只有擁有互斥物件的執行緒才有訪問公共資源的許可權,因為互斥物件只有一個,所以能保證公共資源不會同時被多個執行緒訪問。互斥不僅能實現同一應用程式的公共資源安全共享,還能實現不同應用程式的公共資源安全共享。

程式碼示例:

#include "stdafx.h"
#include<windows.h>
#include<iostream>
using namespace std;

int number = 1; //定義全域性變數
HANDLE hMutex;  //定義互斥物件控制代碼

unsigned long __stdcall ThreadProc1(void* lp)
{
    while (number < 100)
    {
        WaitForSingleObject(hMutex, INFINITE);
        cout << "thread 1 :"<<number << endl;
        ++number;
        _sleep(100);
        ReleaseMutex(hMutex);
    }

    return 0;
}

unsigned long __stdcall ThreadProc2(void* lp)
{
    while (number < 100)
    {
        WaitForSingleObject(hMutex, INFINITE);
        cout << "thread 2 :"<<number << endl;
        ++number;
        _sleep(100);
        ReleaseMutex(hMutex);
    }

    return 0;
}

int main()
{
    hMutex = CreateMutex(NULL, false, "mutex");     //建立互斥物件

    CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
    CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);

    Sleep(10*1000);

    system("pause");
    return 0;
}

執行結果:
這裡寫圖片描述
可以看到,實現了有序輸出,實現了執行緒同步。