執行緒互斥和同步(二 方式)執行緒
執行緒間的同步方法大體可分為兩類:使用者模式和核心模式。顧名思義,核心模式就是指利用系統核心物件的單一性來進行同步,使用時需要切換核心態與使用者態,而使用者模式就是不需要切換到核心態,只在使用者態完成操作。
使用者模式下的方法有:原子操作(例如一個單一的全域性變數),臨界區。核心模式下的方法有:事件,訊號量,互斥量。
下面我們來分別看一下這些方法:
Critical section(臨界區)用來實現“排他性佔有”。適用範圍是單一程序的各執行緒之間。它是:
一個區域性性物件,不是一個核心物件。
快速而有效率。
不能夠同時有一個以上的 critical section 被等待。
無法偵測是否已被某個執行緒放棄。
可以實現執行緒間互斥,不能用來實現同步。
vc下實現:
//CritiCalSection.h
/* 臨界區是指一個訪問共享資源的程式碼段,臨界區物件則是指當用戶使用某個執行緒訪問共享資源時,必須使程式碼段獨佔該資源,不允許其他執行緒訪問該資源。在該執行緒訪問完資源後,其他執行緒才能對資源進行訪問。Windows API提供了臨界區物件的結構體CRITICAL_SECTION,對該物件的使用可總結為如下幾步: 1.InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函式的作用是初始化臨界區,唯一的引數是指向結構體CRITICAL_SECTION的指標變數。 2.EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函式的作用是使呼叫該函式的執行緒進入已經初始化的臨界區,並擁有該臨界區的所有權。這是一個阻塞函式,如果執行緒獲得臨界區的所有權成功,則該函式將返回,呼叫執行緒繼續執行,否則該函式將一直等待,這樣會造成該函式的呼叫執行緒也一直等待。如果不想讓呼叫執行緒等待(非阻塞),則應該使用TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)。 3.LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函式的作用是使呼叫該函式的執行緒離開臨界區並釋放對該臨界區的所有權,以便讓其他執行緒也獲得訪問該共享資源的機會。一定要在程式不適用臨界區時呼叫該函式釋放臨界區所有權,否則程式將一直等待造成程式假死。 4.DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection),該函式的作用是刪除程式中已經被初始化的臨界區。如果函式呼叫成功,則程式會將記憶體中的臨界區刪除,防止出現記憶體錯誤。 */ #ifndef CRTC_SEC_H #define CRTC_SEC_H #include "windows.h" class RAII_CrtcSec { private: CRITICAL_SECTION crtc_sec; public: RAII_CrtcSec() { ::InitializeCriticalSection(&crtc_sec); } ~RAII_CrtcSec() { ::DeleteCriticalSection(&crtc_sec); } RAII_CrtcSec(const RAII_CrtcSec &a) { crtc_sec = a.crtc_sec; } RAII_CrtcSec & operator=(const RAII_CrtcSec &a) { crtc_sec = a.crtc_sec; } void Lock() { ::EnterCriticalSection(&crtc_sec); } void Unlock() { ::LeaveCriticalSection(&crtc_sec); } }; #endif
//main.cpp
//臨界區 #include <windows.h> #include <iostream> #include "CritiCalSection.h" DWORD WINAPI Fun_1(LPVOID p); DWORD WINAPI Fun_2(LPVOID p); unsigned int counter = 0; RAII_CrtcSec cs; int main() { HANDLE h1, h2; h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, 0); std::cout << "Thread 1 started...\n"; h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, 0); std::cout << "Thread 2 started...\n"; CloseHandle(h1); CloseHandle(h2); // system("pause"); return 0; } DWORD WINAPI Fun_1(LPVOID p) { while (true) { cs.Lock(); ++counter; if (counter < 100) { std::cout << "Thread 1 counting " << counter << "...\n"; cs.Unlock(); } else { cs.Unlock(); break; } } return 0; } DWORD WINAPI Fun_2(LPVOID p) { while (true) { cs.Lock(); ++counter; if (counter < 100) { std::cout << "Thread 2 counting " << counter << "...\n"; cs.Unlock(); } else { cs.Unlock(); break; } } return 0; }
2、訊號量(Semaphore):它允許多個執行緒在同一時刻訪問同一資源,但是需要限制在同一時刻訪問此資源的最大執行緒數目, 訊號量增加了資源計數的功能,預定數目的執行緒允許同時進入要同步的程式碼。
Semaphore 被用來追蹤有限的資源。它是:
一個核心物件。
沒有擁有者。
可以具名,因此可以被其他程序開啟。
可以被任何一個執行緒釋放(released)。
既能實現執行緒間互斥,也能實現執行緒間同步。
在跨程序中使用時,如果擁有訊號量的執行緒意外結束,其它程序不會收到通知。
vc下實現:
#include "windows.h"
#include "iostream"
using namespace std;
int num=1;
HANDLE hDemaphore;
unsigned long _stdcall ThreadProc1(void *lpParameter)
{
while(num<100)
{
long count;
WaitForSingleObject(hDemaphore,INFINITE);
cout<<"訊號量1當前計數"<<num<<endl;
num++;
Sleep(3000);
if(!ReleaseSemaphore(hDemaphore,1,&count))
{
if (0 == GetLastError())
{
printf("當前可用資源數:10\n");
return 0;
}
}
cout<<" 訊號量1當前釋放"<<count<<endl;
}
return 0;
}
unsigned long _stdcall ThreadProc2(void *lpParameter)
{
while(num<100)
{
long count;
WaitForSingleObject(hDemaphore,INFINITE);
cout<<"訊號量2當前計數"<<num<<endl;
num++;
Sleep(3000);
if(!ReleaseSemaphore(hDemaphore,1,&count))
{
if (0 == GetLastError())
{
printf("當前可用資源數:10\n");
return 0;
}
}
cout<<" 訊號量2當前釋放"<<count<<endl;
}
return 0;
}
unsigned long _stdcall ThreadProc3(void *lpParameter)
{
while(num<100)
{
long count;
WaitForSingleObject(hDemaphore,INFINITE);
cout<<"訊號量3當前計數"<<num<<endl;
num++;
Sleep(3000);
if(!ReleaseSemaphore(hDemaphore,1,&count))
{
if (0 == GetLastError())
{
printf("當前可用資源數:10\n");
return 0;
}
}
cout<<" 訊號量3當前釋放"<<count<<endl;
}
return 0;
}
int main()
{
HANDLE hThread1=CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);
HANDLE hThread2=CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);
HANDLE hThread3=CreateThread(NULL,0,ThreadProc3,NULL,0,NULL);
hDemaphore=CreateSemaphore(NULL, 1, 3, L"Sem");
CloseHandle(hThread1);
CloseHandle(hThread2);
CloseHandle(hThread3);
while(true)
{
;
}
CloseHandle(hDemaphore);
return 0;
}
3、互斥量(Mutex):採用互斥物件機制。只有擁有互斥物件的執行緒才有訪問公共資源的許可權,因為互斥物件只有一個,所以能保證公共資源不會同時被多個執行緒訪問。互斥不僅能實現同一應用程式的公共資源安全共享,還能實現不同應用程式的公共資源安全共享
互斥比較類似阻塞,關鍵在於互斥可以跨程序的執行緒同步,而且等待一個被鎖住的Mutex可以設定TIMEOUT,不會像Critical Section那樣無法得知臨界區域的情況,而一直死等。很多隻允許應用程式執行一次的例項就是用互斥方法來實現的。當一個互斥物件不再被一個執行緒所擁有,它就處於發訊號狀態。此時首先呼叫waitForsingleobject()的執行緒就成為該互斥物件的擁有者,此互斥物件設為不發訊號狀態。當執行緒呼叫releaseMutex()並傳遞一個互斥物件的控制代碼作為引數時,這種擁有關係就被解除,互斥物件重新進入發訊號狀態。
Mutex 是一個核心物件,可以在不同的執行緒之間實現“排他性佔有”,甚
至即使那些執行緒分屬不同程序。它是:
一個核心物件。
如果擁有 mutex 的那個執行緒結束,則會產生一個 “abandoned” 錯誤資訊。
可以具名,因此可以被其他程序開啟。
只能被擁有它的那個執行緒釋放(released)。
在跨程序中使用時,如果擁有互斥器的程序意外結束,其它程序會收到一個WAIT_ABANDOEND訊息。
vc下實現:
//互斥物件的使用方法和c++標準庫的mutex類似,互斥物件使用完後應記得釋放。
//main.cpp
#include <windows.h>
#include <iostream>
DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p);
HANDLE h_mutex;
unsigned int counter = 0;
int main()
{
h_mutex = CreateMutex(nullptr, false, nullptr);
HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr);
std::cout << "Thread 1 started...\n";
HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr);
std::cout << "Thread 2 started...\n";
CloseHandle(h1);
CloseHandle(h2);
//
//CloseHandle(h_mutex);
system("pause");
return 0;
}
DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_mutex, INFINITE);
if (counter < 100)
{
++counter;
std::cout << "Thread 1 counting " << counter << "...\n";
ReleaseMutex(h_mutex);
}
else
{
ReleaseMutex(h_mutex);
break;
}
}
return 0;
}
DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_mutex, INFINITE);
if (counter < 100)
{
++counter;
std::cout << "Thread 2 counting " << counter << "...\n";
ReleaseMutex(h_mutex);
}
else
{
ReleaseMutex(h_mutex);
break;
}
}
return 0;
}
4、 事件(Event ) :通過通知操作的方式來保持執行緒的同步,還可以方便實現對多個執行緒的優先順序比較的操作
Event object 通常使用於 overlapped I/O,或用來設計某些自定
義的同步物件。它是:
一個核心物件。
可是用來實現執行緒的互斥與同步。
可以具名,因此可以被其他程序開啟。
在跨程序中使用時,如果擁有訊號量的執行緒意外結束,其它程序也不會受到通知。
vc下實現:
/*
事件物件是一種核心物件,使用者在程式中使用核心物件的有無訊號狀態來實現執行緒的同步。使用事件物件的步驟可概括如下:
1.建立事件物件,函式原型為:
HANDLE WINAPI CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,
_In_ BOOL bManualReset,
_In_ BOOL bInitialState,
_In_opt_ LPCTSTR lpName
);
如果該函式呼叫成功,則返回新建立的事件物件,否則返回NULL。函式引數的含義如下:
-lpEventAttributes:表示建立的事件物件的安全屬性,若設為NULL則表示該程式使用的是預設安全屬性。
-bManualReset:表示所建立的事件物件是人工重置還是自動重置。若設為true,則表示使用人工重置,在呼叫執行緒獲得事件物件所有權後用戶要顯式地呼叫ResetEvent()將事件物件設定為無訊號狀態。
-bInitialState:表示事件物件的初始狀態。如果為true,則表示該事件物件初始時為有訊號狀態,則執行緒可以使用事件物件。
-lpName:表示事件物件的名稱,若為NULL,則表示建立的是匿名事件物件。
2.若事件物件初始狀態設定為無訊號,則需呼叫SetEvent(HANDLE hEvent)將其設定為有訊號狀態。ResetEvent(HANDLE hEvent)則用於將事件物件設定為無訊號狀態。
3.執行緒通過呼叫WaitForSingleObject()主動請求事件物件,該函式原型如下:
DWORD WINAPI WaitForSingleObject(
_In_ HANDLE hHandle,
_In_ DWORD dwMilliseconds
);
該函式將在使用者指定的事件物件上等待。如果事件物件處於有訊號狀態,函式將返回。否則函式將一直等待,直到使用者所指定的事件到達。
*/
#if 0
#include <windows.h>
#include <iostream>
DWORD WINAPI Fun_1(LPVOID p);
DWORD WINAPI Fun_2(LPVOID p);
HANDLE h_event;
unsigned int counter = 0;
int main()
{
h_event = CreateEvent(nullptr, true, false, nullptr);
SetEvent(h_event);
HANDLE h1 = CreateThread(nullptr, 0, Fun_1, nullptr, 0, nullptr);
std::cout << "Thread 1 started...\n";
HANDLE h2 = CreateThread(nullptr, 0, Fun_2, nullptr, 0, nullptr);
std::cout << "Thread 2 started...\n";
CloseHandle(h1);
CloseHandle(h2);
//
system("pause");
return 0;
}
DWORD WINAPI Fun_1(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_event, INFINITE);
ResetEvent(h_event);
if (counter < 100)
{
++counter;
std::cout << "Thread 1 counting " << counter << "...\n";
SetEvent(h_event);
}
else
{
SetEvent(h_event);
break;
}
}
return 0;
}
DWORD WINAPI Fun_2(LPVOID p)
{
while (true)
{
WaitForSingleObject(h_event, INFINITE);
ResetEvent(h_event);
if (counter < 100)
{
++counter;
std::cout << "Thread 2 counting " << counter << "...\n";
SetEvent(h_event);
}
else
{
SetEvent(h_event);
break;
}
}
return 0;
}
#endif
就使用效率來說,臨界區的效率是最高的,因為它不是核心物件,而其它的三個都是核心物件,要藉助作業系統來實現,效率相對來說就比較低。但如果要跨程序使用還是要用到互斥量、事件物件和訊號量。