1. 程式人生 > >執行緒同步總結--臨界區 事件 互斥量 訊號量

執行緒同步總結--臨界區 事件 互斥量 訊號量

在WIN32中,同步機制主要有以下幾種:

  • 臨界區(Critical section)

  • 事件(Event);

  • 互斥量(mutex);

  • 訊號量(semaphore);

 

臨界區(Critical section)

臨界區(Critical Section)指的是一個訪問共用資源(例如:共用裝置或是共用儲存器)的程式片段,而這些共用資源有無法同時被多個執行緒訪問的特性。

在有一個執行緒進入臨界區後,其他所有試圖訪問此臨界區的執行緒將被掛起,並一直持續到進入臨界區的執行緒離開。臨界區在被釋放後,其他執行緒可以繼續搶佔,並以此達到用原子方式操作共享資源的目的。

臨界區可以用於執行緒間的互斥,但不可以用於同步。

除臨界區外,事件、互斥量和訊號量都是核心物件。

一旦執行緒進入一個臨界區,則它就可以一再的重複進入該臨界區,當然每個進入操作都必須對應離開操作。

初始化

銷燬

進入臨界區

離開臨界區

InitializeCriticalSection

DeleteCriticalSection

EnterCriticalSection

LeaveCriticalSection

也就是EnterCriticalSection( ),可以巢狀。

但是千萬不要在臨界區中呼叫 sleep(),或任何 Wait..() 函式。

臨界區的缺點是:沒有辦法知道進入臨界區中的那個執行緒是生是死。如果那個執行緒在進入臨界區後當掉了,而且沒有退出來,那麼系統就沒有辦法消除掉此臨界區。

 

事件(Event)

 用來通知執行緒有一些事件已發生,從而啟動後繼任務的開始。

       在建立事件物件時可以設定為“自動重置事件”或“手動重置事件”。當一個手動重置事件被觸發的時候,正在等待該事件的所有執行緒都將變成可排程狀態;而當一個自動重置事件物件被觸發的時候,只有一個正在等待該事件的執行緒會變成可排程狀態,隨後該事件物件自動變為未觸發態,但是如果被觸發時沒有等待該事件物件的執行緒,那麼該事件物件會保持觸發狀態,直至遇到第一個等待該事件物件的執行緒。

建立事件

銷燬事件

獲得事件控制代碼

觸發事件

使事件未觸發

CreateEvent

CloseHandle

OpenEvent

SetEvent

ResetEvent

舉例:

//使用事件機制同步執行緒的例子
//設定三個執行緒,一個主執行緒,一個讀執行緒和一個寫執行緒,
//讀執行緒必須在寫執行緒寫之後才能讀,主執行緒必須在讀執行緒讀之後才能結束
/*
實現:定義兩個事件, evRead, evFinish;讀執行緒等待evRead, 主執行緒等待evFinish.
*/
#include <iostream>
#include <Windows.h>
#include <process.h>
using namespace std;
HANDLE evRead, evFinish;
void ReadThread(void* param){
    //等待讀事件
    WaitForSingleObject(evRead, INFINITE);
    //讀操作
    cout << "reading" << endl;
    //激發事件evFinish
    SetEvent(evFinish);
}
void WriteThread(void* param){
    //寫操作
    cout << "writing" << endl;
    //喚醒讀事件
    SetEvent(evRead);
}
int main()
{
    //建立兩個事件,並初始化為未激發狀態
    evRead = CreateEvent(NULL, false, false, NULL);
    evFinish = CreateEvent(NULL, false, false, NULL);
    
    //建立讀執行緒和寫執行緒
    _beginthread(ReadThread, 0, NULL);
    _beginthread(WriteThread, 0, NULL);
    //等待事件evFinish
    WaitForSingleObject(evFinish, INFINITE);
    cout << "the program is end." << endl;
    return 0;
}

 

互斥量(mutex)

互斥量(mutex)是指用來防止兩條執行緒同時對同一公共資源(比如全域性變數)進行讀寫的機制。

互斥量跟臨界區很相似,只有擁有互斥物件的執行緒才具有訪問資源的許可權由於互斥物件只有一個,因此就決定了任何情況下此共享資源都不會同時被多個執行緒所訪問。當前佔據資源的執行緒在任務處理完後應將擁有的互斥物件交出,以便其他執行緒在獲得後得以訪問資源。互斥量比臨界區複雜。因為使用互斥不僅僅能夠在同一應用程式不同執行緒中實現資源的安全共享,而且可以在不同應用程式的執行緒之間實現對資源的安全共享。 

建立互斥量

開啟互斥量

觸發互斥量

清理互斥量

CreateMutex

OpenMutex

ReleaseMutex

CloseHandle

Mutexes 用途和 Critical Section 非常類似,執行緒擁有 mutex 就好象執行緒進入 critical section 一樣,但是它犧牲速度以增加彈性

一旦沒有任何執行緒擁有那個 mutex,這個 mutex 便處於激發狀態。

它與臨界區的區別是:

1. Mutexes 操作要比 Critical Section 費時的多。

2. Mutexes 可以跨程序使用,Critical Section 則只能在同一程序中使用。

3. 等待一個 Mutex 時,你可以指定"結束等待"的時間長度,而 Critical Section 則不行。

說明:

1. Mutex 的擁有權:

Mutex 的擁有權並非屬於那個產生它的執行緒,而是那個最後對些 Mutex 進行 WaitXXX() 操作並且尚未進行 ReleaseMutex() 操作的執行緒。

2. Mutex 被捨棄:

如果執行緒在結束前沒有呼叫 ReleaseMutex(),比如執行緒呼叫了 EXitThread() 或者因為當掉而結束。這時的 mutex 不會被摧毀,而是被視為"未被擁有"以及"未被激發"的狀態,在下一個 WaitXXX() 中執行緒會被以WAIT_ABANDONED_0 (WAIT_ABANDONED_0_n + 1 )來通知。

3. 最初擁有者:

CreateMutex(),第二個引數 bInitialOwner,允許你指定現行執行緒是否立刻擁有產生出來的 mutex。

示例:

第一個程式:建立互斥量並等待使用者輸入後就觸發互斥量。

#include <stdio.h>
#include <conio.h>
#include <windows.h>
const char MUTEX_NAME[] = "Mutex_MoreWindows";
int main()
{
    HANDLE hMutex = CreateMutex(NULL, TRUE, MUTEX_NAME); //建立互斥量
    printf("互斥量已經建立,現在按任意鍵觸發互斥量\n");
    getch();
    //exit(0);
    ReleaseMutex(hMutex);
    printf("互斥量已經觸發\n");
    CloseHandle(hMutex);
    return 0;
}

第二個程式:先開啟互斥量,成功後就等待並根據等待結果作相應的輸出。

#include <stdio.h>
#include <windows.h>
const char MUTEX_NAME[] = "Mutex_MoreWindows";
int main()
{
    HANDLE hMutex = OpenMutex(MUTEX_ALL_ACCESS, TRUE, MUTEX_NAME); //開啟互斥量
    if (hMutex == NULL)
    {
        printf("開啟互斥量失敗\n");
        return 0;
    }
    printf("等待中....\n");
    DWORD dwResult = WaitForSingleObject(hMutex, 20 * 1000); //等待互斥量被觸發
    switch (dwResult)
    {
    case WAIT_ABANDONED:
        printf("擁有互斥量的程序意外終止\n");
        break;
    case WAIT_OBJECT_0:
        printf("已經收到訊號\n");
        break;
    case WAIT_TIMEOUT:
        printf("訊號未在規定的時間內送到\n");
        break;
    }
    CloseHandle(hMutex);
    return 0;
}

 

訊號量Semaphore

訊號量Semaphore用於保持在0至指定最大值之間的一個計數值。

訊號量物件對執行緒的同步方式與前面幾種方法不同,訊號量允許多個執行緒同時使用共享資源 ,這與作業系統中的PV操作相同。它指出了同時訪問共享資源的執行緒最大數目。它允許多個執行緒在同一時刻訪問同一資源,但是需要限制在同一時刻訪問此資源的最大執行緒數目。在用CreateSemaphore()建立訊號量時即要同時指出允許的最大資源計數和當前可用資源計數。一般是將當前可用資源計數設定為最大資源計數,每增加一個執行緒對共享資源的訪問,當前可用資源計數就會減1,只要當前可用資源計數是大於0的,就可以發出訊號量訊號。但是當前可用計數減小到0時則說明當前佔用資源的執行緒數已經達到了所允許的最大數目, 不能在允許其他執行緒的進入,此時的訊號量訊號將無法發出。執行緒在處理完共享資源後,應在離開的同時通過ReleaseSemaphore()函式將當前可用資源計數加1。在任何時候當前可用資源計數決不可能大於最大資源計數。 

建立訊號量

銷燬

開啟訊號量

遞增計數

遞減計數

CreateSemaphore

CloseHandl

OpenSemaphore

ReleaseSemaphore

WaitForSingleObject

 在經典多執行緒問題中設定一個訊號量和一個臨界區。用訊號量處理主執行緒與子執行緒的同步,用關鍵段來處理各子執行緒間的互斥。詳見程式碼:

#include <stdio.h>
#include <process.h>
#include <windows.h>
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//訊號量與關鍵段
HANDLE            g_hThreadParameter;
CRITICAL_SECTION  g_csThreadCode;
int main()
{
    printf("     經典執行緒同步 訊號量Semaphore\n");
    printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
    //初始化訊號量和關鍵段
    g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL);//當前0個資源,最大允許1個同時訪問
    InitializeCriticalSection(&g_csThreadCode);
    HANDLE  handle[THREAD_NUM];    
    g_nNum = 0;
    int i = 0;
    while (i < THREAD_NUM)
    {
        handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
        WaitForSingleObject(g_hThreadParameter, INFINITE);//等待訊號量>0
        ++i;
    }
    WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
    
    //銷燬訊號量和關鍵段
    DeleteCriticalSection(&g_csThreadCode);
    CloseHandle(g_hThreadParameter);
    for (i = 0; i < THREAD_NUM; i++)
        CloseHandle(handle[i]);
    return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
    int nThreadNum = *(int *)pPM;
    ReleaseSemaphore(g_hThreadParameter, 1, NULL);//訊號量++
    Sleep(50);//some work should to do
    EnterCriticalSection(&g_csThreadCode);
    ++g_nNum;
    Sleep(0);//some work should to do
    printf("執行緒編號為%d  全域性資源值為%d\n", nThreadNum, g_nNum);
    LeaveCriticalSection(&g_csThreadCode);
    return 0;
}

 

 總結
1. 互斥量與臨界區的作用非常相似,但互斥量是可以命名的,也就是說它可以跨越程序使用。所以建立互斥量需要的資源更多,所以如果只為了在程序內部是用的話使用臨界區會帶來速度上的優勢並能夠減少資源佔用量 。因為互斥量是跨程序的互斥量一旦被建立,就可以通過名字開啟它。 
2. 互斥量(Mutex),訊號量(Semaphore),事件(Event)都可以被跨越程序使用來進行同步資料操作,而其他的物件與資料同步操作無關,但對於程序和執行緒來講,如果程序和執行緒在執行狀態則為無訊號狀態,在退出後為有訊號狀態。所以可以使用WaitForSingleObject來等待程序和 執行緒退出。 
3. 通過互斥量可以指定資源被獨佔的方式使用,但如果有下面一種情況通過互斥量就無法處理,比如現在一位使用者購買了一份三個併發訪問許可的資料庫系統,可以根據使用者購買的訪問許可數量來決定有多少個執行緒/程序能同時進行資料庫操作,這時候如果利用互斥量就沒有辦法完成這個要求,訊號燈物件可以說是一種資源計數器

參考文章:

https://blog.csdn.net/morewindows/article/details/7538247

https://blog.csdn.net/sunshinewave/article/details/50851061