這段時間一直在做Windows下的項目開發, 最近花了點時間翻了翻書, 總結一下Windows常用的線程同步方法.
一、線程同步--SendMessage()
1. SendMessage()是一種簡單方便的視窗和工作者線程間的資料同步的方法
2. 當工作者線程將資料準備好後,可以通過調用SendMessage()將訊息發送給指定的視窗,這時發送線程將被阻塞直到SendMessage()返回.
LRESULT SendMessage(
HWND hWnd, // 目標視窗控制代碼
UINT Msg, // 被發送的訊息
WPARAM wParam, // 第一個訊息參數
LPARAM lParam // 第二個訊息參數
);
注意: 類似功能的PostMessage是不阻塞的, 將訊息發送到UI主線程訊息佇列直接返回.
二、線程同步--臨界地區(Critical Section)
1. 臨界區一次只允許一個線程取得對某個資料區的訪問權,保證同一時刻只能有一個線程進入臨界區(獲得臨界區對象)。
2. 臨界區非常適合對一個進程中的資料的訪問,因為它們的速度很快.
3. 臨界區對象不是核心對象,它不由作業系統的低級組件管理,而且不能使用控制代碼來操縱。
4. 在進程中建立一個臨界區,即在進程中分配一個CRITICAL_SECTION資料結構,該臨界區結構的分配必須是全域的,這樣該進程的不同線程就能訪問它.
5. 在使用臨界區同步線程之前,必須調用InitializeCriticalSection來初始化臨界區,釋放資源之前,只需要初始化一次。
6. 調用VOID EnterCriticalSection()來進入臨界區,如果臨界區對象被別的線程佔用,該線程阻塞進入睡眠,直到佔有臨界區對象的線程釋放臨界區對象,且在被喚醒之前,系統不會給它分配CPU。
7. 調用VOID TryEnterCriticalSection()來進入臨界區,如果臨界區對象被別的線程佔用,返回FALSE,線程不進入睡眠狀態,繼續執行.
8. 調用LeaveCriticalSection()離開臨界區,當擁有臨界區所有權的線程調用LeaveCriticalSection放棄所有權時,系統只喚醒正等待中的一個線程,給它所有權,其它線程則繼續睡眠。
9. 注意, EnterCriticalSection() 和 LeaveCriticalSection()務必保證成對調用, 當一個佔有臨界區的線程多次調用EnterCriticalSection(), 將使得臨界區對象的引用計數增加一, 如果想要徹底放棄臨界區擁有權,必須調用同樣多的LeaveCriticalSection()
10. 調用DeleteCriticalSection釋放臨界區資源.
typedef struct _RTL_CRITICAL_SECTION
{
long LockCount;
long RecursionCount;
HANDLE oWningThread; //from the thread's ClientId->UniqueThread
HANDLE LockSemaphore;
DWORD SpinCount;
}PTL_CRITICAL_SECTION,*PRTL_CRITICAL_SECTION;
typedef PRTL_CRITICAL_SECTION LPCRITICAL_SECTION
typedef RTL_CRITICAL_SECTION CRITICAL_SECTION
VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
BOOL TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection)
範例程式碼:
TryEnterCriticalSection(&critical_section)
TODO: 這裡對同步資料進行操作
LeaveCriticalSection(&critical_section)
if (TryEnterCriticalSection(&critical_section))
{
TODO: 這裡對同步資料進行操作
LeaveCriticalSection(&critical_section);
}
三、線程同步--核心對象
1. 適用多個進程的線程間資料同步.
2. 同樣也能實現單個進程中的線程同步,但是效率要比使用臨界區方式要差些。
4. 下列核心對象可用來同步線程:
進程,Processes
線程,Threads
檔案,Files
控制台輸入,Console input
檔案變化通知,File change notifications
互斥量,Mutexes
訊號量,Semaphores
事件(自動重設事件和手動重設事件),Events
可等的計時器(只用於Window NT4或更高),Waitable timers
Jobs
5. 在所有的核心對象中,事件核心對象是個最基本的對象
6. 使用CreateEvent()函數建立事件核心對象
7. 使用OpenEvent()函數開啟事件核心對象
8. CloseHandle() 來釋放核心對象
9. 使用SetEvent()將事件改為已通知狀態:
10. 使用ResetEvent()函數時,可以將該事件改為未通知狀態:
11. 在工作者現程中使用WaitForSingleObject()函數用來檢測hHandle事件的訊號狀態
函數功能描述:建立或開啟一個命名的或無名的事件對象
函數原型:
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全屬性
BOOL bManualReset, // 複位方式
BOOL bInitialState, // 初始狀態
LPCTSTR lpName // 對象名稱
);
DWORD WaitForSingleObject( HANDLE hHandle, DWORD dwMilliseconds);
參數hHandle是一個事件的控制代碼,第二個參數dwMilliseconds是時間間隔。如果時間是有訊號狀態返回WAIT_OBJECT_0,如果時間超過dwMilliseconds值但時間事件還是無訊號狀態則返回WAIT_TIMEOUT
但如果參數dwMilliseconds為INFINITE時函數將直到相應時間事件變成有訊號狀態才返回,否則就一直等待下去,直到WaitForSingleObject有返回直才執行後面的代碼
範例程式碼:
人工重設的事件使用: 主控線程當設定SetEvent後, 背景工作執行緒A和背景工作執行緒B將同時開始工作,比如可以同時對一段資料進行並行讀操作.
主控線程:
HANDLE hEvent; // 全域
hEvent = CreateEvent (NULL, TRUE, FALSE, NULL) ; // 建立事件對象, 告訴系統是建立一個人工重設事件對象
SetEvent (hEvent) ; // 控制背景工作執行緒MyThreadA/MyThreadB開始工作
背景工作執行緒A和B:
UINT MyThreadA( LPVOID pParam )
{
for(;;)
{
WaitForSingleObject(hEvent,INFINITE);
// 背景工作執行緒做一些事情
}
return 0;
}
UINT MyThreadB( LPVOID pParam )
{
for(;;)
{
WaitForSingleObject(hEvent,INFINITE);
// 背景工作執行緒做一些事情
}
return 0;
}
自動重設的事件使用: 主控線程當設定SetEvent後, 背景工作執行緒A和背景工作執行緒B將有一個開始工作,而另一個將繼續等待狀態, 直到一個操作完成另一個背景工作執行緒才開始工作. 比如多個線程對一段資料進行寫操作,需要把它們序列化
主控線程:
HANDLE hEvent; // 全域
hEvent = CreateEvent (NULL, FALSE, FALSE, NULL) ; // 建立事件對象, 告訴系統是建立一個自動重設事件對象
SetEvent (hEvent) ; // 控制背景工作執行緒MyThreadA/MyThreadB開始工作
背景工作執行緒A和B:
UINT MyThreadA( LPVOID pParam )
{
for(;;)
{
WaitForSingleObject(hEvent,INFINITE);
// 這裡做一些事情
SetEvent (hEvent);
}
return 0;
}
UINT MyThreadB( LPVOID pParam )
{
for(;;)
{
WaitForSingleObject(hEvent,INFINITE);
// 這裡做一些事情
SetEvent (hEvent);
}
return 0;
}