Windows線程同步的方法

來源:互聯網
上載者:User

Summary:

對於多線程編程,一個很重要的問題就是解決由資料共用引起的資料競爭的問題,通過一定的線程同步的方法能避免資料競爭。在Win32多線程中,同步方法包括使用者態同步方式:InterLock、CriticalSection、SRWLock和核心態同步方式:Event、Semaphore、Mutex等。

本文主要目的是收集這些同步的方法和涉及到API的含義和使用,可能不會涉及到更深入的比如何時使用何種方式更好以及為什麼,更深入的問題,單獨在以後積累並記錄。

一、資料競爭的例子

在分析同步的方法之前,先給出要解決的資料競爭的例子:

#include "stdafx.h"#include <stdio.h>#include <Windows.h>#include <process.h>long g = 0;#define THREAD_COUNT10// 線程數#define ACCESS_TIMES10000000// 訪問共用變數的次數,增大其值,增加資料競爭發生的可能性void __cdecl ThreadProc(void *para){printf("sub thread started\n");for (int i = 0;i < ACCESS_TIMES;i++)g = g + 1;printf("sub thread finished\n");_endthread();// 可以省略,隱含會調用。}int main(int argc, char* argv[]){HANDLE hThread[THREAD_COUNT];for(int i =0;i < THREAD_COUNT;i++)hThread[i] = (HANDLE)_beginthread(ThreadProc,0,NULL);for(int i = 0;i < THREAD_COUNT;i++)WaitForSingleObject(hThread[i],INFINITE);// 檢查結果if (g == ACCESS_TIMES*THREAD_COUNT)printf("Correct Result!\n");else printf("Error Result!\n");}

如果程式輸出Error Result,說明發生了資料競爭。

二、使用者態同步

需要注意的是,由於使用者態到核心態的轉換需要一定的開銷,所以如果能在使用者態進行同步控制,盡量選擇使用者態的方式。

(1)InterLocked原子操作

InterLocked是一系列的方法,提供了原子操作的實現。原子操作比較容易理解,直接調用對應的API就能進行簡單的同步了,但是這種方式,顯然只能適用於簡單的預定義的一些操作方法,比如自增、自減、加法等,具體有哪些方法,可以參考MSDN(http://msdn.microsoft.com/zh-cn/site/ms684122)

對於上面的資料競爭的例子,顯然可以使用自增的原子操作來解決了,對於自增操作,MS提供了InterlockedIncrement()和InterlockedIncrement64()來完成原子操作,分別操作32位和64位的整數(long,long long),參數為要進行自增的變數的地址。只需要將上述代碼中,“g=g+1"修改為:

InterlockedIncrement(&g); // g = g + 1;

即可解決資料競爭問題。

(2)Critical Section臨界區

百度百科:不論是硬體臨界資源,還是軟體臨界資源,多個進程必須互斥地對它進行訪問。每個進程中訪問臨界資源的那段代碼稱為臨界區(Critical Section)(臨界資源是一次僅允許一個進程使用的共用資源)。每次只准許一個進程進入臨界區,進入後不允許其他進程進入。不論是硬體臨界資源,還是軟體臨界資源,多個進程必須互斥地對它進行訪問。

與臨界區相關的函數有:

EnterCriticalSection LeaveCriticalSectionInitializeCriticalSectionDeleteCriticalSectionTryEnterCriticalSection

EnterCriticalSection和LeaveCriticalSection是一對操作,表示進入和離開臨界區,同步控制一段代碼的訪問,即在這兩個函數之間調用的代碼,每次只允許一個線程執行。

InitializeCriticalSection和DeleteCriticalSection也是一對操作,分別是初始化和刪除臨界區(變數),在使用臨界區的時候,需要定義一個臨界區變數,代表一個臨界區資源。所以,一個程式可以使用多個臨界區變數來進行不同的程式碼片段的保護。在調用EnterCriticalSection的時候,如果臨界區被其它線程佔有,那麼當前線程會等待資源,知道其它線程退出臨界區。TryEnterCriticalSection函數就是用來嘗試進入臨界區,如果無法進入臨界區(臨界區被佔有),那麼返回FALSE,不會阻塞線程。

下面是使用臨界區解決上述問題的代碼:

#include "stdafx.h"#include <stdio.h>#include <Windows.h>#include <process.h>long g = 0;CRITICAL_SECTION g_cs;// 定義臨界區#define THREAD_COUNT10// 線程數#define ACCESS_TIMES10000000// 訪問共用變數的次數,增大其值,增加資料競爭發生的可能性void __cdecl ThreadProc(void *para){printf("sub thread started\n");for (int i = 0;i < ACCESS_TIMES;i++){EnterCriticalSection(&g_cs);// 進入臨界區g = g + 1;LeaveCriticalSection(&g_cs);// 退出臨界區}printf("sub thread finished\n");_endthread();// 可以省略,隱含會調用。}int main(int argc, char* argv[]){InitializeCriticalSection(&g_cs);// 初始化臨界區HANDLE hThread[THREAD_COUNT];for(int i =0;i < THREAD_COUNT;i++)hThread[i] = (HANDLE)_beginthread(ThreadProc,0,NULL);for(int i = 0;i < THREAD_COUNT;i++)WaitForSingleObject(hThread[i],INFINITE);DeleteCriticalSection(&g_cs);// 刪除臨界區// 檢查結果if (g == ACCESS_TIMES*THREAD_COUNT)printf("Correct Result!\n");else printf("Error Result!\n");}

可見,臨界區能保護一個代碼塊,使用起來比原子操作更靈活。

(3)SRWLOCK讀寫鎖

關於讀寫鎖:http://baike.baidu.com/view/2214179.htm(百度百科)

讀寫鎖實際是一種特殊的自旋鎖,它把對共用資源的訪問者劃分成讀者和寫者,讀者只對共用資源進行讀訪問,寫者則需要對共用資源進行寫操作。這種鎖相對於自旋鎖而言,能提高並發性,因為在多處理器系統中,它允許同時有多個讀者來訪問共用資源,最大可能的讀者數為實際的邏輯CPU數。寫者是排他性的,一個讀寫鎖同時只能有一個寫者或多個讀者(與CPU數相關),但不能同時既有讀者又有寫者。

說明:在win32中,自旋鎖是核心定義,且只能在核心狀態下使用的一種鎖機制。所以一般的程式不會用到,需要一定的要求。而SRWLOCK讀寫鎖是使用者態下的讀寫鎖。

從使用上,SRWLOCK和臨界區的使用非常類似,讀寫鎖和臨界區的主要區別是,讀寫鎖對共用資源的訪問區分了讀者和寫者分離的功能。所以,SRWLOCK比臨界區多了一些方法。

與SRWLOCK相關的函數主要有:(具體可參考http://msdn.microsoft.com/zh-cn/library/aa904937.aspx介紹所有的讀寫鎖相關的API)

AcquireSRWLockShared
AcquireSRWLockExclusive
ReleaseSRWLockShared
ReleaseSRWLockExclusive
InitializeSRWLock
TryAcquireSRWLockExclusive
TryAcquireSRWLockShared
SleepConditionVariableSRW

其中,AcquireSRWLockShared和AcquireSRWLockExclusive表示擷取讀鎖和擷取寫鎖(共用鎖定和獨佔鎖定)。ReleaseSRWLockShared和ReleaseSRWLockExclusive表示釋放讀鎖和寫鎖。和臨界區一樣,InitializeSRWLock是初始化。但是,SRWLock沒有提供刪除讀寫鎖的方法,不需要刪除。TryAcquireSRWLockExclusive和TryAcquireSRWLockShared也是用於非阻塞的方式擷取讀鎖和寫鎖,失敗返回FALSE。SleepConditionVariableSRW在下面再介紹。需要說明的是:TryAcquireSRWLockExclusive和TryAcquireSRWLockShared是Win7才開始提供支援的,詳細資料參考MSDN。

下面是使用讀寫鎖解決上面的資料競爭的問題:(說明:這裡只需要擷取寫鎖,更多的程式可能會需要同時使用讀鎖和寫鎖)

#include "stdafx.h"#include <stdio.h>#include <Windows.h>#include <process.h>long g = 0;SRWLOCK g_srw;// 定義讀寫鎖#define THREAD_COUNT10// 線程數#define ACCESS_TIMES10000000// 訪問共用變數的次數,增大其值,增加資料競爭發生的可能性void __cdecl ThreadProc(void *para){printf("sub thread started\n");for (int i = 0;i < ACCESS_TIMES;i++){AcquireSRWLockExclusive(&g_srw);// 擷取寫鎖g = g + 1;ReleaseSRWLockExclusive(&g_srw);// 釋放寫鎖}printf("sub thread finished\n");_endthread();// 可以省略,隱含會調用。}int main(int argc, char* argv[]){InitializeSRWLock(&g_srw);// 初始化讀寫鎖HANDLE hThread[THREAD_COUNT];for(int i =0;i < THREAD_COUNT;i++)hThread[i] = (HANDLE)_beginthread(ThreadProc,0,NULL);for(int i = 0;i < THREAD_COUNT;i++)WaitForSingleObject(hThread[i],INFINITE);// 檢查結果if (g == ACCESS_TIMES*THREAD_COUNT)printf("Correct Result!\n");else printf("Error Result!\n");}

讀寫鎖的適用情況:讀寫鎖適合於對資料結構的讀次數比寫次數多得多的情況。更多相關問題,參考關於讀寫鎖的特性等的分析介紹。

(4)Condition Variable條件變數

MSDN:http://msdn.microsoft.com/zh-cn/site/ms682052

條件變數一開始是在Linux中有,Window平台是從Vista才開始支援條件變數(所以XP不支援)。

關於條件變數本身的解釋,參考百度百科(http://baike.baidu.com/view/4025952.htm),當然,百度百科裡面都是說的Linux,但是概念本身其實是類似的。如下:

條件變數是利用線程間共用的全域變數進行同步的一種機制,主要包括兩個動作:一個線程等待"條件變數的條件成立"而掛起;另一個線程使"條件成立"(給出條件成立訊號)。為了防止競爭,條件變數的使用總是和一個互斥鎖結合在一起。其實,在windows下,條件變數的使用總是和讀寫鎖或臨界區結合使用(因為Linux下沒有臨界區,所以這裡只說了互斥鎖)。

和條件變數相關的函數有:

Condition variable function
InitializeConditionVariable
SleepConditionVariableCS
SleepConditionVariableSRW
WakeAllConditionVariable
WakeConditionVariable

具體使用說明可以參考MSDN,由於條件變數和這裡的資料競爭的例子很難結合起來,這裡就不舉例了,而且它單獨是無法完成同步的,所以這裡也沒必要單獨作為一種同步的方法來說明,關於條件變數的使用場合和使用方法,參考其它內容。

三、核心態同步

上面介紹的都是使用者態的同步方法,win32多線程還提供了一些核心態同步的方式。從效能上來說,核心態同步方式比使用者態更低,原因是使用者態到核心態的轉換是有開銷的。但是核心態的優點在於是可以跨進程同步的,所以不僅僅是線程同步方式,也是一種進程同步方式。

(1)核心對象和狀態

在瞭解核心態同步之前,首先需要瞭解很重要的兩個函數:WaitForSingleObject和WaitForMultipleObjects。

1. 核心對象

核心對象只是核心分配的一個記憶體塊,並且只能由該核心訪問。該記憶體塊是一種資料結構,它的成員負責維護該對象的各種資訊。有些資料成員(如安全性描述符、使用計數等)在所有物件類型中是相同的,但大多數資料成員屬於特定的物件類型。例如,進程對象有一個進程I D 、一個基本優先順序和一個結束代碼,而檔案對象則擁有一個位元組位移、一個共用模式和一個開啟模式。

參考:

http://www.cnblogs.com/fangyukuan/archive/2010/08/31/1813117.html

http://i.mtime.com/MyFighting/blog/1793762/

總之,這裡要提到的核心態同步的對象,都是屬於核心對象,包括進程對象和線程對象也是屬於核心對象。另外要知道的是,核心對象使用相應的建立函數建立,返回都是控制代碼,即HANDLE對象。

2. 核心同步對象

在Windows NT核心對象中,提供了五種核心同步對象(Kernel Dispatcher Object),為:Event(事件)、Semaphore(號誌/訊號量)、Mutex(互斥)、Timer(定時器)、Thread(線程)。

關於核心同步對象可以參考:http://hi.baidu.com/klksys/blog/item/2c470ad25808cdd6a9ec9aaa.html

3. 核心對象的狀態

在任何時刻,任何對象都處於兩種狀態中的一種:訊號態或非訊號態(參考上面的連結的說明,沒有找到官方證實這句話,但是至少對於核心同步對象,所有的對象應該都是有這兩個狀態的)。有時候,這兩個狀態稱為受信狀態(signaled state)和非受信狀態(nonsignaled state)或者通知狀態和未通知狀態。

(參考http://st251256589.blog.163.com/blog/static/16487644920111244054511/)。

到了這裡,我們就可以討論WaitForSingleObject了。WaitForSingleObject的參數是一個核心物件控點,它的作用是:Waits until the specified object is in the signaled state or the time-out interval elapses。即等待指定的對象處於受信狀態或者出現逾時,等待,表明如果執行WaitForSingleObject的時候,對象處於非受信狀態,那麼當前線程處於阻塞狀態。當然,WaitForMultipleObjects的作用就是等待多個狀態了。

說明,WaitForSingleObject對於某些核心對象是由副作用的,比如對於訊號量,等待成功會使得訊號量減1。

參考MSDN:http://msdn.microsoft.com/zh-cn/site/ms687032可以知道WaitForSingleObject的用法和它能等待的所有核心對象:

Change notification
Console input
Event
Memory resource notification
Mutex
Process
Semaphore
Thread

Waitable timer

其中加粗的幾個核心對象是多線程編程中會遇到的(三個核心態同步對象和一個線程對象)。理解了signaled state和nonsignaled state之後,下面的三種核心態同步方式就很容易理解了。

(2)Event事件

MSDN:http://msdn.microsoft.com/zh-cn/site/ms682655 (Event Objects)

事件核心對象包括兩種:人工重設的事件和自動重設的事件。

當人工重設事件得到通知時,等待該事件的所有線程成為可調度線程;它沒有成功等待副作用。
當自動重設的事件得到通知時,等待該事件的線程中只有一個線程變為可調度線程。其成功等待的副作用是該對象自動重設為未通知狀態。

事件核心對象通過CreateEvent建立,初始可以是通知或未通知狀態。

人工事件一般用於一個線程通知另一個線程或者一個線程通知多個線程進行某一操作。自動事件適用於保護資源在同一時間只能有一個線程可以訪問,它能保證只有一個線程被啟用。

事件對象分為命名事件對象和未命名物件(named and unnamed event objects)。

和事件對象相關的函數有:CreateEvent/OpenEvent、ResetEvent、SetEvent、PulseEvent等。

其中,CreateEvent建立或開啟一個事件對象,其中,四個參數分別為安全屬性(核心對象都有安全屬性),是否為人工重設事件,初始狀態(TRUE表示signaled,FALSE表示nonsignled),事件對象的名字,如果為NULL,建立一個未命名事件對象。如果指定name的事件已經存在,則獲得EVENT_ALL_ACCESS的存取權限,第一個參數部分有效,後兩個參數忽略。OpenEvent用於開啟已存在的事件(所以一般用CreateEvent即可)。ResetEvent/SetEvent分別設定事件的狀態為nonsignaled和signaled。PulseEvent根據MSDN不可信,所以不推薦使用(相當於reset然後set的功能,但是並不可靠)。

總之,對於事件來說,常用的方法是CreateEvent,ResetEvent,SetEvent,然後利用WaitForSingleObject來等待事件(變成singled狀態)。

仍然針對上面的資料競爭的例子,使用事件解決的方法是:

#include "stdafx.h"#include <stdio.h>#include <Windows.h>#include <process.h>long g = 0;HANDLE g_event;// 事件控制代碼#define THREAD_COUNT10// 線程數#define ACCESS_TIMES100000// 訪問共用變數的次數,增大其值,增加資料競爭發生的可能性void __cdecl ThreadProc(void *para){printf("sub thread started\n");for (int i = 0;i < ACCESS_TIMES;i++){WaitForSingleObject(g_event, INFINITE);// 等待事件ResetEvent(g_event);// 重設事件,讓其他線程繼續等待(相當於擷取鎖)g = g + 1;SetEvent(g_event);// 設定事件,讓其他線程可以擷取事件(相當於釋放鎖)}printf("sub thread finished\n");_endthread();// 可以省略,隱含會調用。}int main(int argc, char* argv[]){g_event = CreateEvent(NULL, FALSE, FALSE, NULL);// 建立事件核心對象SetEvent(g_event);HANDLE hThread[THREAD_COUNT];for(int i = 0;i < THREAD_COUNT;i++)hThread[i] = (HANDLE)_beginthread(ThreadProc,0,NULL);for(int i = 0;i < THREAD_COUNT;i++)WaitForSingleObject(hThread[i],INFINITE);// 檢查結果if (g == ACCESS_TIMES*THREAD_COUNT)printf("Correct Result!\n");else printf("Error Result!\n");}

說明:實際運行就會發現,核心態事件對象同步的方法比使用者態的方法的效率低很多,如果這裡的ACCESS_TIMES如果太大,已耗用時間相比使用者態的方法多很多。當然,再次強調,這裡都是用這一個例子,只是為了分析各種方法實現同步的實現,實際應用,顯然是有所取捨的,不同的同步方法有不同的合適的使用情景。

(3)Semaphore訊號量

MSDN:http://msdn.microsoft.com/zh-cn/site/ms685129(Semaphore Objects)

訊號量用來對資源進行計數。它包含兩個32位值,一個表示能夠使用的最大資源數量,一個表示當前可用的資源數量。
訊號量的使用規則為:如果當前資源數量大於0,發出訊號量訊號;如果當前資源數量是0,不發出訊號量訊號;不允許當前資源數量為負值
 當前資源數量不能大於最大訊號數量

當調用等待函數時,它會檢查訊號量的當前資源數量。如果它的值大於0,那麼計數器減1,調用線程處於可調度狀態。如果當前資源是0,則調用函數的線程進入等待狀態。當另一個線程對訊號量的當前資源通過ReleaseSemaphore進行遞增時,系統會記住該等待線程,並將其變為可調度狀態。

對於訊號量,相關的函數有:CreateSemaphore/OpenSemaphore、ReleaseSemaphore。WaitForSingleObject對於訊號量,成功等待的副作用是使得訊號量減1。從某種角度理解,事件相當於最大計數為1的訊號量。

下面的例子是使用最大計數為1的訊號量來解決上面的資料競爭問題:

#include "stdafx.h"#include <stdio.h>#include <Windows.h>#include <process.h>long g = 0;HANDLE g_semaphore;// 訊號量物件控點#define THREAD_COUNT10// 線程數#define ACCESS_TIMES100000// 訪問共用變數的次數,增大其值,增加資料競爭發生的可能性void __cdecl ThreadProc(void *para){printf("sub thread started\n");for (int i = 0;i < ACCESS_TIMES;i++){WaitForSingleObject(g_semaphore, INFINITE);// 等待訊號量// 擷取訊號量後計數會減1g = g + 1;ReleaseSemaphore(g_semaphore,1, NULL);// 訊號量加1}printf("sub thread finished\n");_endthread();// 可以省略,隱含會調用。}int main(int argc, char* argv[]){g_semaphore = CreateSemaphore(NULL, 0, 1, NULL);// 建立訊號量,初始計數為0,最大計數為1ReleaseSemaphore(g_semaphore,1, NULL);// 將計數設定為1HANDLE hThread[THREAD_COUNT];for(int i = 0;i < THREAD_COUNT;i++)hThread[i] = (HANDLE)_beginthread(ThreadProc,0,NULL);for(int i = 0;i < THREAD_COUNT;i++)WaitForSingleObject(hThread[i],INFINITE);// 檢查結果if (g == ACCESS_TIMES*THREAD_COUNT)printf("Correct Result!\n");else printf("Error Result!\n");}

(4)Mutex互斥(互斥對象,互斥體)

MSDN:http://msdn.microsoft.com/zh-cn/site/ms684266(Mutex Objects)

互斥器保證線程擁有對單個資源的互斥訪問權。互斥對象類似於關鍵代碼區(臨界區),但它是一個核心對象。 互斥器不同於其他核心對象,它有一個“線程所有權”的概念。它如果被某個線程等待成功,就屬於該線程。

由於和臨界區和讀寫鎖很類似,使用也是很類似的。和Mute相關的函數主要有:CreateMutex/OpenMutex,ReleaseMutex。很顯然,Create是建立,Open是開啟已存在的命名互斥對象,ReleaseMutex是釋放互斥對象。幾乎和臨界區的函數一樣,當然,用WaitForSingleObject等待互斥體,類似於進入臨界區的操作了。

代碼如下:

#include "stdafx.h"#include <stdio.h>#include <Windows.h>#include <process.h>long g = 0;HANDLE g_mutex;// 互斥物件控點#define THREAD_COUNT10// 線程數#define ACCESS_TIMES100000// 訪問共用變數的次數,增大其值,增加資料競爭發生的可能性void __cdecl ThreadProc(void *para){printf("sub thread started\n");for (int i = 0;i < ACCESS_TIMES;i++){WaitForSingleObject(g_mutex, INFINITE);// 等待互斥對象g = g + 1;ReleaseMutex(g_mutex);// 釋放互斥對象}printf("sub thread finished\n");_endthread();// 可以省略,隱含會調用。}int main(int argc, char* argv[]){g_mutex = CreateMutex(NULL, FALSE, NULL);// 建立互斥核心對象HANDLE hThread[THREAD_COUNT];for(int i = 0;i < THREAD_COUNT;i++)hThread[i] = (HANDLE)_beginthread(ThreadProc,0,NULL);for(int i = 0;i < THREAD_COUNT;i++)WaitForSingleObject(hThread[i],INFINITE);// 檢查結果if (g == ACCESS_TIMES*THREAD_COUNT)printf("Correct Result!\n");else printf("Error Result!\n");}

總結:這裡介紹了使用者態和核心態的同步對象和基本函數的使用。這裡只是為了示範其使用和理解其概念,針對實際應用,需要根據實際的case選擇合適的方法進行同步。

相關文章:

http://archive.cnblogs.com/a/2223856/

http://www.cnblogs.com/TravelingLight/archive/2011/10/07/2200912.html

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.