windows支持4種類型的同步對象,能夠用來同步由併發運行的線程所執行的操做:c++
臨界區編程
互斥量windows
事件api
信號量數組
MFC在名爲CCriticalSection、CMutex、CEvent和CSemaphore的類中封裝了這些對象。下面分別對這些同步對象進行介紹。安全
臨界區多線程
最簡單類型的線程同步對象就是臨界區。臨界區用來串行化對由兩個或者多個線程共享的資源的訪問。這些線程必須屬於相同的進程,由於臨界區不能跨越進程的邊界工做。併發
臨界區背後的思想就是,每一個獨佔性地訪問一個資源的線程能夠在訪問那個資源以前鎖定臨界區,訪問完成以後解除鎖定。若是線程B試圖鎖定當前線程A鎖定的臨界區,線程B將阻塞直到該臨界區空閒。阻塞時,線程B處在一個十分有效的等待狀態,它不消耗處理器時間。函數
互斥量工具
Mutex是單詞mutually和exclusive的縮寫。與臨界區同樣,互斥量也是用來得到對由兩個或者更多線程共享的資源的獨佔性訪問的。與臨界區不一樣的是,互斥量能夠用來同步在相同進程或者不一樣進程上運行的線程。對於進程內線程同步的須要,臨界區通常要優於互斥量,由於臨界區更快,可是若是但願同步在兩個或者多個不一樣進程上運行的線程,那麼互斥量就更合適了。
互斥量和臨界區還有另外有一個差異。若是一個線程鎖定了臨界區而終止時沒有解除臨界區的鎖定,那麼等待臨界區空閒的其餘線程將無限期地阻塞下去。然而,若是鎖定互斥量的線程不能在其終止前解除互斥量的鎖定,那麼系統將認爲互斥量被「放棄」了,並自動釋放該互斥量,這樣等待進程就能夠繼續進行。
事件
MFC的CEvent類封裝了Win32事件對象。一個事件不僅是操做系統內核中的一個標記。在任何特定的時間,事件只能處在兩種狀態中的一種:設置或者重置。設置狀態事件也能夠認爲是處於信號狀態,重置狀態事件也能夠認爲是處於非信號狀態。CEvent::SetEvent設置一個事件,而CEvent::ResetEvent將事件重置。相關函數CEvent::PulseEvent能夠在一次操做中設置和清除一個事件。
有時事件被描述爲「線程觸發器」。一個線程調用CEvent::Lock在一個事件上阻塞,等待該事件變爲設置狀態。另外一個線程設置事件,從而喚醒該等待線程。設置事件就像按下觸發器。它解除等待線程的阻塞並容許該線程繼續執行。一個事件可能有一個或者多個在事件上阻塞的線程,若是你的代碼正確,那麼當該事件變爲設置狀態時,全部的等待線程都將被喚醒。
Windows支持兩種不一樣類型的事件:自動重置事件和手動重置事件。它們之間的差異很是細微,但其意義倒是深遠的。當在自動重置事件上阻塞的線程被喚醒時,該事件被自動重置爲信號狀態。手動重置事件不能自動重置,它必須使用編程方式重置。用於選擇自動重置事件仍是手動重置事件——以及一旦作出選擇以後如何使用它們——的規則以下:
\1) 若是事件只觸發一個線程,那麼使用自動重置事件和使用SetEvent喚醒等待線程。這裏不須要調用ResetEvent,由於線程被喚醒那一刻事件將自動重置。
\2) 若是事件將觸發兩個或者多個線程,那麼使用手動重置線程和使用PulseEvent喚醒全部的等待線程。並且,不須要調用ResetEvent,由於PulseEvent在喚醒線程後將重置事件。
使用手動重置事件來觸發多個線程是相當重要的。爲何?由於自動重置事件將在其中一個線程被喚醒的那一刻被重置,所以它只觸發一個線程。使用PulseEvent來按下手動重置事件上的觸發器也是至關重要的。若是使用SetEvent和ResetEvent,就有保證全部的等待線程都被喚醒。PulseEvent不只可以設置和重置事件,並且還確保了全部在事件上等待的線程在重置事件以前被喚醒。
與互斥量同樣,事件能夠用來協調在不一樣進程上運行的線程,對於跨越進程邊界的事件,必須給它指定一個名稱。
那麼,怎樣使用事件來同步線程呢?例如,線程A向緩衝區填充數據,而線程B須要對緩衝區的數據進行處理。假定線程B必須等待來自線程A的一個信號(緩衝區已初始化並準備工做)。這時,自動重置事件是完成這項工做的絕好工具。
自動重置事件適用於觸發單線程,但若是與線程B平行的線程對C緩衝的數據進行了徹底不一樣的操做,那會怎麼樣呢?這就須要手動重置事件一同喚醒線程B和C,由於自動重置事件只能喚醒其中的一個或者另外一個,而不能都喚醒 。
再次重申,自動重置事件和CEvent::SetEvent釋放在事件上阻塞的單個線程,手動重置事件和CEvent::PulseEvent釋放多個線程。
信號量
最後一種同步化對象是信號量。若是任何一個線程鎖定了事件、臨界區和互斥對象,Lock就會阻塞它們,在這個意義上,這3種對象具備這樣的特性:」要麼有,要麼什麼都沒有「。信號量則不一樣,它始終保存有可用資源數量的資源數。鎖定信號量會減小資源數,釋放信號量則增長資源數。只有在線程試圖鎖定資源數爲0的信號量時,線程纔會被阻塞。在這種狀況下,直到另外一個線程釋放信號量,資源數隨之增長時,或者直到指定的超時時間期滿時,該線程纔會被釋放。信號量能夠用來同步同一進程中的線程,也能夠同步不一樣進程中的線程。
爲何使用線程同步?
同步能夠保證在一個時間內只有一個線程對某個資源(如操做系統資源等共享資源)有控制權。共享資源包括全局變量、公共數據成員或者句柄等。同步還可使得有關聯交互做用的代碼按必定的順序執行。
線程同步的方式?
同步對象有:CRITICAL_SECTION (臨界區),Event(事件),Mutex(互斥對象),Semaphores(信號量)。
本文重點講解CRITICAL_SECTION (臨界區)。
臨界區,說白了,就是「鎖」。看過星爺的《破壞王》的朋友都知道,那個送外賣的小子,就是靠自創絕招「無敵風火輪」將大師兄戰勝,抱得美人歸。「無敵風火輪」的本質就是:鎖!
怎麼鎖?
這裏有四個關鍵函數:InitializeCriticalSection EnterCriticalSection LeaveCriticalSection DeleteCriticalSection來完成此機制。
使用臨界區對象的時候,首先要定義一個臨界區對象CriticalSection:
CRITICAL_SECTION CriticalSection;
而後,初始化該對象:InitializeCriticalSection(&CriticalSection);
若是一段程序代碼須要對某個資源進行同步保護,則這是一段臨界區代碼。在進入該臨界區代碼前調用EnterCriticalSection函數,這樣,其餘線程都不能執行該段代碼,若它們試圖執行就會被阻塞。
完成臨界區的執行以後,調用LeaveCriticalSection函數,其餘的線程就能夠繼續執行該段代碼。
簡要實例
下面的代碼中,若是不加CRITICAL_SECTION ,有可能形成在線程1給data設置完名字後,線程2給data設置年齡,形成了數據紊亂,因此有必要使用同步機制,將其鎖住,保證數據的安全。
class Data { private: CString Name; int Age; public: void SetName(const CString& name) { Name = name; } void SetAge(int age) { Age = age; } void GetName(CString &name) { name = Name; } void GetAge(int &age) { age = Age; } }; Data g_data; //全局變量 CRITICAL_SECTION CriticalSection; //線程函數 DWORD WINAPI ThreadProc( LPVOID lpParameter ) { EnterCriticalSection(&CriticalSection); data.SetName("趙星星"); data.SetAge(20); LeaveCriticalSection(&CriticalSection); } int main() { InitializeCriticalSection(&CriticalSection); //建立線程,執行線程函數 //...... DeleteCriticalSection(&CriticalSection); return 0; }
真鎖?假鎖?
能夠定義CRITICAL_SECTION 數組:CRITICAL_SECTION g_Critical[10];
CRITICAL_SECTION 沒有超時的概念,若是函數LeaveCriticalSection不被調用,則其餘線程將無限期的等待。容易形成死鎖。
CRITICAL_SECTION 屬於輕量級的線程同步對象,相對於mutex來講,它的效率會高不少。mutex能夠用於進程之間的同步,CRITICAL_SECTION只在同一個進程有效。
實際上,CRITICAL_SECTION 鎖的是代碼段,若是代碼段中有對資源的佔用,只是間接的鎖住了該資源,咱們也能夠稱之爲「假鎖」。
Windows下進程內部的各個線程之間的同步不須要藉助內核對象,Windows提供的默認在用戶模式下的線程同步工具。
互鎖函數爲多線程同步訪問共享變量提供了一個簡單的機制。若是變量在共享內存,不一樣進程的線程也可使用此機制。
互鎖函數對共享變量的操做是原子的,這個原子性體如今保證多線程在同一個時刻只能有一個線程得到對該同步變量的操做權限。
(1)InterlockedExchangeAdd()
LONG __cdecl InterlockedExchangeAdd( _Inout_ LONG volatile *Addend, _In_ LONG Value ); LONGLONG __cdecl InterlockedExchangeAdd64( _Inout_ LONGLONG volatile *Addend, _In_ LONGLONG Value ); //Addend:指向一個32位變量的指針; //Value:共享變量上要加的值; //Return value:返回修改前變量的值;
InterlockedExchangeAdd互鎖函數提供了對變量進行加法操做,保證了同一時刻只有一個線程對這個變量進行加法操做。Value是正數的時候進行加法操做,是負數的時候進行減法操做。
(2)InterlockedIncrement()&InterlockedDecrement()
LONG __cdecl InterlockedIncrement( _Inout_ LONG volatile *Addend ); LONG __cdecl InterlockedDecrement( _Inout_ LONG volatile *Addend ); //Addend:指向一個32位變量的指針; //Return value:修改前變量的值;
InterlockedIncrement互鎖函數對一個32變量進行增1操做,InterlockedDecrement則進行減1操做。保證線程之間的互斥的進行訪問。這兩個函數都是16位和64位版本。
(3)InterlockedExchange()
LONG __cdecl InterlockedExchange( _Inout_ LONG volatile *Target, _In_ LONG Value ); //Target:指向一個32位變量的指針; //Value:要替換的值; //Return Value:修改以前的值; PVOID __cdecl InterlockedExchangePointer( _Inout_ PVOID volatile *Target, _In_ PVOID Value ); //Target:指向一個32位變量的指針的指針; //Value:要替換的指針的值; //ReturnValue:修改以前的值;
InterlockedExchange函數把第一個參數指向的內存地址的值,以原子的方式替換爲第二個參數的值。並返回原來的值。InterlockedExchangePointer替換的是指針而已。
InterlockedExchange函數還有8位,16位和64位的版本;
(4)InterlockedCompareExchange()
LONG __cdecl InterlockedCompareExchange( _Inout_ LONG volatile *Destination, _In_ LONG Exchange, _In_ LONG Comparand ); //Destination:指向當前值的指針; //Exchange:比較成功後要替換的值; //Comparand:和當前值進行比較的值; //Return Value:修改以前的值; PVOID __cdecl InterlockedCompareExchangePointer( _Inout_ PVOID volatile *Destination, _In_ PVOID Exchange, _In_ PVOID Comparand );
InterlockedCompareExchange函數會將Destination指向的當前值和Comparand進行比較,若是相同會將Destination指向的值替換爲Exchange的值,不然*Destination保持不變。函數的返回值爲修改以前的值。
當你建立一個線程時,其實那個線程是一個循環,不像上面 那樣只運行一次的。這樣就帶來了一個問題,在那個死循環裏要找到合適的條件退出那個死循環,那麼是怎麼樣實現它的呢?
在Windows裏每每是採用事件的 方式,它的實現原理以下:在那個死循環裏不斷地使用 WaitForSingleObject函數來檢查事件是否知足,若是知足就退出線程,不知足就繼續運行。
對函數進行解釋。
CreateEvent是建立windows事件的意思,做用主要用在判斷線程退出,線程鎖定方面.
返回值:若是函數調用成功,函數返回事件對象的句柄,若是函數調用失敗,函數返回值爲NULL
EVENT有兩種狀態:發信號,不發信號。
SetEvent:將EVENT置爲發信號。
ResetEvent:將EVENT置爲不發信號。
WaitForSingleObject():等待,直到參數所指定的OBJECT成爲發信號狀態時才返回,OBJECT能夠是EVENT,也能夠是其它內核對象。
WaitForSingleObject用法:
(1)函數功能描述:用來檢測hHandle事件的信號狀態,在某一線程中調用該函數時,線程暫時掛起,若是在掛起的dwMilliseconds毫秒內,線程所等待的對象變爲有信號狀態,則該函數當即返回;若是超時時間已經到達dwMilliseconds毫秒,但hHandle所指向的對象尚未變成有信號狀態,函數照樣返回。參數dwMilliseconds有兩個具備特殊意義的值:0和INFINITE。若爲0,則該函數當即返回;若爲INFINITE,則線程一直被掛起,直到hHandle所指向的對象變爲有信號狀態時爲止。
(2)函數原型:
DWORD WINAPI WaitForSingleObject ( __in HANDLE hHandle, // 句柄 __in DWORD dwMilliseconds // 時間間隔 );
(3)參數:
hHandle:對象句柄。能夠指定一系列的對象,如Event、Job、Memory resource notification、Mutex、Process、Semaphore、Thread、Waitable timer等。
dwMilliseconds:定時時間間隔,單位爲milliseconds(毫秒).
(1)若是指定一個非零值,函數處於等待狀態直到hHandle標記的對象被觸發,或者時間到了。
(2)若是dwMilliseconds爲0,對象沒有被觸發信號,函數不會進入一個等待狀態,它老是當即返回。
(3)若是dwMilliseconds爲INFINITE,對象被觸發信號後,函數纔會返回。
(4)返回值:執行成功,返回值指示出引起函數返回的事件。可能的返回值:
WAIT_ABANDONED 0x00000080:當hHandle爲mutex時,若是擁有mutex的線程在結束時沒有釋放核心對象會引起此返回值。
WAIT_OBJECT_0 0x00000000 :指定的對象出有信號狀態。
WAIT_TIMEOUT 0x00000102:等待超時。
WAIT_FAILED 0xFFFFFFFF :出現錯誤,可經過GetLastError獲得錯誤代碼。
具體實例
//test.h #include <windows.h> // for HANDLE HANDLE m_HandleWaitForTest; m_HandleWaitForTest= CreateEvent(NULL,FALSE,FALSE,NULL); // test.cpp void fun() { ......//其餘操做 ResetEvent(m_HandleWaitForTest);//程序剛開始事件清零,設爲無信號狀態 ......//其餘操做 QtConcurrent::run(this, &MotionTest::TestEvent);//開啓一個線程 ......//其餘操做 if (!(WaitForSingleObject(m_HandleWaitForTest, 5000) == WAIT_OBJECT_0)) { return; } } void MotionTest::TestEvent() { if(Test()) { SetEvent(m_HandleWaitForTest);//設置爲有信號,爲後續判斷該線程是否執行完作準備 } else { MessageInformation(tr("test failed"), false); } }
WaitForMultipleObjects的用法
DWORD WaitForMultipleObjects( DWORD nCount, const HANDLE* lpHandles, BOOL bWaitAll, DWORD dwMilliseconds);
其中參數
nCount 句柄的數量 最大值爲MAXIMUM_WAIT_OBJECTS(64)
HANDLE 句柄數組的指針。
HANDLE 類型能夠爲(Event,Mutex,Process,Thread,Semaphore )數組
BOOL bWaitAll 等待的類型,若是爲TRUE 則等待全部信號量有效在往下執行,FALSE 當有其中一個信號量有效時就向下執行
DWORD dwMilliseconds 超時時間 超時後向執行。 若是爲WSA_INFINITE 永不超時。若是沒有信號量就會在這死等。
具體事例
m_threadShow = std::thread(std::mem_fn(&MainWindow::ShowData), this); MainWindow::~MainWindow() { SetEvent(m_KillEvent); if(m_threadShow.joinable()) m_threadShow.join(); delete ui; } //好比開一個線程,可用WaitForMultipleObjects,在析構中SetEvent killEvent事件,便可釋放線程函數 //其餘狀況只要有m_showEvent則會向下執行 void MainWindow::ShowData() { while(1) { HANDLE Status[2] = {m_KillEvent, .m_showEvent}; if(WaitForMultipleObjects(2, Status, FALSE, INFINITE) == WAIT_OBJECT_0)//第一個事件發生 { break; } //........... } }
windows api中提供了一個互斥體,功能上要比臨界區強大。Mutex是互斥體的意思,當一個線程持有一個Mutex時,其它線程申請持有同一個Mutex會被阻塞,所以能夠經過Mutex來保證對某一資源的互斥訪問(即同一時間最多隻有一個線程訪問)。
調用CreateMutex能夠建立或打開一個Mutex對象,其原型以下
HANDLE CreateMutex ( LPSECURITY_ATTRIBUTES lpMutexAttributes, BOOL bInitialOwner, LPCTSTR lpName );