# 17.3 用于線程同步的對象
在幾乎所有的線程使用中,數據都是幾個線程共享的.當有兩個或以上的線程試圖訪問同一個數據的時候,無論這個數據是一個對象還是一個資源,這種訪問都應該被同步,以避免數據在同一時刻被超過1個線程訪問或者修改.因為應用程序中充滿了所謂的不變量,比如,對于一個鏈表來說,我們總認為它的第一個元素是有效的,每個元素都指向它的下一個元素,最后的一個元素是空指針.但是在對鏈表進行插入新元素操作的時候,有一小段時間間隔,這個所謂不變量是被打破的.這時候假如有兩個線程同時在使用這個鏈表,如果沒有進行數據同步的動作,就會出現不可知的問題.因此你必須保證,在你插入元素這一小段時間間隔內,沒有別的線程在訪問同樣的數據.
保證所有的共享數據被各個訪問它的線程快速的并且是以一個合理的順序訪問是程序員自己的責任.因此這一小節我們來介紹一下wxWidgets提供了哪些類來幫助程序員達到這個目的.
wxMutex
這個名字來源于mutual exclusion(共有的互斥量),它是最簡單的一種數據同步手段.它可以保證同一個時刻只有一個線程在訪問某一部分數據.要獲取數據的訪問權,線程必須調用wxMutex::Lock函數,這將阻塞當前線程的執行直到它所請求的數據已經不再有任何別的線程使用.而在它開始使用這個數據以后,別的線程對 wxMutex::Lock的調用同樣將被阻塞,直至當前使用的線程調用wxMutex::Unlock函數釋放它所使用的資源.盡管你可以直接使用 wxMutex的Lock和Unlock函數,我們還是推薦你使用wxMutexLocker類來使用wxMutex,這將確保你不會忘記在Lock以后調用Unlock函數(譯者注:你可以想像假如你忘了Unlock的后果,呵呵),因為這兩個函數被隱藏在wxMutexLocker類的構造函數和析構函數中,因此,即使發生了異常,wxMutexLocker類仍然會在自己被釋放的時候進行Unlock.
下面的代碼中,我們確信MyApp有一個wxMutex類型的變量m_mutex:
```
void MyApp::DoSomething()
{
wxMutexLocker lock(m_mutex);
if (lock.IsOk())
{
... do something
}
else
{
... we have not been able to
... acquire the mutex, fatal error
}
}
```
使用互斥量有三個重要的規則:
1. 線程不可以鎖定已經被鎖定的互斥量(不允許互斥量遞歸). 盡管有些系統允許你這樣做,但這是不可移植的.
2. 線程不允許解鎖別的線程鎖定的互斥量. 如果你需要這個功能,參考我們馬上會講到的信號量機制.
3. 如果你的線程即使無法鎖定互斥量也還有別的事情可以做,你應該先使用wxMutex::TryLock函數判斷是否可以鎖定.這個函數是立即返回的,返回值為可以鎖定(wxMUTEX_NO_ERROR)或者不可鎖定(wxMUTEX_DEAD_LOCK或 wxMUTEX_BUSY). 這在主線程中尤其有用,因為主線程(GUI線程)是不可以被阻塞的,否則它將不能響應任何用戶的輸入.
死鎖
如果兩個線程在互相等待對方已經鎖定的互斥量,我們稱之為發生了死鎖.舉例來說,假如線程A已經鎖定了互斥量1,線程B已經鎖定了互斥量 2,線程A正等待鎖定互斥量2,而線程B正等待鎖定互斥量1,那么,他們兩個人將無限期的等待下去,在某些系統上,如果出現這種情況,Lock或者 Unlock或者TryLock函數將返回錯誤碼wxMUTEX_DEAD_LOCK,但是在另外一些系統上,除非你把整個程序殺死,否則他們將一直等下去.
解決死鎖的方法有一下兩種:
* 修改順序.一個一致的互斥量鎖定順序將減少死鎖發生的概率.在前面的例子中,如果線程A和線程B都要求先鎖定互斥量1再鎖定互斥量2,則死鎖將不會發生.
* 使用TryLock. 在成功鎖定第一個互斥量以后,在后續的互斥量鎖定之前都使用TryLock函數判斷,如果TryLock返回失敗,解鎖第一個然后重新開始鎖定第一個. 這種方法系統開銷較大,但是如果修改順序的方法有明顯的缺陷或者導致你的代碼亂七八糟,你可以考慮使用這種方法.
wxCriticalSection
關鍵區域用來保證某一段代碼在某一個時刻只被一個線程執行,而前面介紹的互斥量則用來保證互斥量在某一個時刻只被一個線程鎖定.他們之間是非常相似的,除了在某些系統上,互斥量是系統范圍內的變量而關鍵區域只在本應用程序范圍內有效.在這樣的系統上,使用關鍵區域的效率會比使用互斥量高一點點.也因為這些細微的差別,他們的一些術語也略有不同,互斥量稱為鎖定(或者裝載)和解鎖(或者卸載),而關鍵區域稱為進入或者離開.
關鍵區域也有對應的wxCriticalSectionLocker對象,出于和wxMutexLocker同樣的原因,你應該盡量使用它而不要直接使用wxCriticalSection的函數.
wxCondition
所謂條件變量wxCondition,是用來指示共享數據的某些條件已經滿足.比如,你可以使用它來指示一個消息隊列已經有數據到來.而共享數據本身(在這里指的這個消息隊列)通常還需要另外使用一個互斥量來保護.
你可以通過鎖定互斥量,檢測隊列有無數據,然后釋放信號量這樣的循環來進行消息隊列數據的處理,不過如果隊列里一直沒有數據,這樣的作法也太浪費了,時間全部浪費在鎖定和解鎖互斥量上面了.象這種情況,最好是使用條件變量,這樣消息處理線程就可以被阻塞直到等到別的線程把事件放入事件隊列以后發出通知事件.
多個線程可能都在等待同一個條件,這時你可以選擇喚醒一個線程還是喚醒多個線程,喚醒一個線程的函數是Signal,喚醒所有正在等待的線程的函數是Broadcast.如果有多個條件都是由同一個wxCondition通知的,你必須使用Broadcast函數,否則可能某個線程被喚醒了但是卻什么也做不了,因為它的條件還沒有滿足,而另外的可以滿足條件的那個線程卻無法喚醒了.
wxCondition使用舉例
我們來假設一下我們有兩個線程:
一個是生產線程,它負責產生10個元素并且將其放入隊列,然后發送隊列滿信號并且在繼續填充元素之前等待隊列空信號.
一個是消費線程,它在收到隊列滿信號的時候移除隊列中所有的元素.
我們需要一個互斥量m_mutex,用來保護整個隊列和兩個條件變量:m_isFull和m_isEmpty.這個互斥量被傳遞給兩個條件變量的構造函數作為參數.另外你需要總是顯示判斷條件是否滿足,然后再開始等待通知,因為可能在你還沒有開始等待之前,已經有一個信號通知了,由于你還沒有等待,那個信號就丟失了.
我們來看看生產線程的Entry函數的偽代碼:
```
while ( notDone )
{
wxMutexLocker lock(m_mutex) ;
while( m_queue.GetCount() > 0 )
{
m_isEmpty.Wait() ;
}
for ( int i = 0 ; i < 10 ; ++i )
{
m_queue.Append( wxString::Format(wxT("Element %d"),i) ) ;
}
m_isFull.Signal();
}
```
消費線程:
```
while ( notDone )
{
wxMutexLocker lock(m_mutex) ;
while( m_queue.GetCount() == 0 )
{
m_isFull.Wait() ;
}
for ( int i = queue.GetCount() ; i > 0 ; i )
{
m_queue.RemoveAt( i ) ;
}
m_isEmpty.Signal();
}
```
Wait函數首先Unlock其內部的互斥量 ,然后等待條件被通知.當它被通知喚醒時,會首先再次鎖定內部的信號量,因此數據同步時非常嚴格滿足的.
另外,在Wait函數被喚醒之后再次檢測條件是否滿足也是必要的,因為在信號被發送和線程被喚醒之間可能發生某些事情,導致條件又一次不滿足了;另外,系統有時候也會產生一些假的信號導致Wait函數返回.
Signal可能在Wait之前發生,正象pthread中的那樣,這時這個信號會丟失.因此如果你想要確定你沒有錯過任何信號,你必須保證和條件變量綁定的互斥量在最開始就處于鎖定狀態,并且在你調用Signal函數之前再次嘗試鎖定它,這意味著對Signal的調用將被阻塞直到另外一個線程調用了Wait函數.
OK,上面的這段話讀起來比較費勁,我們來看一個例子,在這個例子中,主線程創建了一個工作線程,工作線程的Signal函數直到主線程調用了Wait以后才能被調用:
```
class MySignallingThread : public wxThread
{
public:
MySignallingThread(wxMutex *mutex, wxCondition *condition)
{
m_mutex = mutex;
m_condition = condition;
Create();
}
virtual ExitCode Entry()
{
... do our job ...
// 告訴其它線程我們馬上就要退出了.
// 我們必須先鎖定信號量,這個動作會阻塞自己
// 直到主線程調用了Wait
wxMutexLocker lock(m_mutex);
m_condition.Broadcast(); // 我們只有一個線程在等待,所以等同于Signal()
return 0;
}
private:
wxCondition *m_condition;
wxMutex *m_mutex;
};
void TestThread()
{
wxMutex mutex;
wxCondition condition(mutex);
// 互斥量應該先出于鎖定狀態
mutex.Lock();
// 先創建和運行工作線程,注意這個線程不能退出
// 除非我們解鎖了互斥量
MySignallingThread *thread =
new MySignallingThread(&mutex, &condition);
thread->Run();
// Wait工作線程退出,Wait函數將自動解鎖和它綁定的互斥量
// 因此工作線程可以繼續直至發出Signal并且終至自己.
condition.Wait();
// 我們收到了Signal就可以退出了.
}
```
當然上面的這個例子指示出于演示如何實現條件變量中第一個Singal在第一個Wait之后執行,如果單就代碼例子實現的功能來說,我們應該直接使用一個聯合線程,然后在主線程調用wxThread::Join函數就可以了.
wxSemaphore
信號量(wxSemaphore)可以通俗的看成一個互斥量和一個記數器的結合,它和記數器最大的不同在于信號量的值可以被任何線程更改,而不僅僅是擁有它的那個線程.所以你也可以把信號量看作是一個沒有主人的記數器.
如果一個線程調用信號量的Wait函數,這個調用將阻塞,除非記數器當前為一個正數,然后Wait函數將記數器減一,然后返回.而對Post函數的調用則將增加記數器的值然后返回.
wxWidgets實現的信號量還有一個額外的特性,你可以在其構造函數中指定一個記數器的最大值,默認為0表明最大值沒有限制,如果你給定了一個最大值,而Post函數的調用使得當前的記數器超過了這個最大值,你將會得到一個wxSEMA_OVERFLOW錯誤.讓我們再回到前面說的用信號量實現特殊互斥的描述:
* 一個可以被不同的線程鎖定和解鎖的互斥量可以通過一個記數器最大值為1的信號量實現,互斥量的Lock函數等同于信號量的Wait函數而互斥量的Unlock函數等同于信號量的Post函數.
* 前一個線程調用Lock(Wait)發現是一個整數值,于是減一,然后立即繼續.
* 第二個線程調用Lock發現是零,將必須等待某個線程(不一定是前一個線程)調用Unlock(Post).
你可以在wxWidgets自帶的samples/thread中找到一個用來演示多線程編程的例子.如下圖所示.在這個例子中,你可以啟動,停止,暫停,恢復線程的運行.它演示了一個工作線程周期性的通過wxPostEvent往主程序發送事件,一個進度條對話框用來指示當前進度并在進度到達最后的時候取消工作線程的運行.

- 第一章 介紹
- 1.1 為什么要使用wxWidgets?
- 1.2 wxWidgets的歷史
- 1.3 wxWidgets社區
- 1.4 wxWidgets和面向對象編程
- 1.5 wxWidgets的體系結構
- 1.6 許可協議
- 第一章小結
- 第二章 開始使用
- 2.1 一個小例子
- 2.2 應用程序類
- 2.3 Frame窗口類
- 2.4 事件處理函數
- 2.5 Frame窗口的構造函數
- 2.6 完整的例子
- 2.7 wxWidgets程序一般執行過程
- 2.8 編譯和運行程序
- 第二章小結
- 第三章 事件處理
- 3.1 事件驅動編程
- 3.2 事件表和事件處理過程
- 3.3 過濾某個事件
- 3.4 掛載事件表
- 3.5 動態事件處理方法
- 3.6 窗口標識符
- 3.7 自定義事件
- 第三章小結
- 第四章 窗口的基礎知識
- 4.1 窗口解析
- 4.2 窗口類概覽
- 4.3 基礎窗口類
- 4.4 頂層窗口
- 4.5 容器窗口
- 4.6 非靜態控件
- 4.7 靜態控件
- 4.8 菜單
- 4.9 控制條
- 第四章小結
- 第五章繪畫和打印
- 5.1 理解設備上下文
- 5.2 繪畫工具
- 5.3 設備上下文中的繪畫函數
- 5.4 使用打印框架
- 5.5 使用wxGLCanvas繪制三維圖形
- 第五章小節
- 第六章處理用戶輸入
- 6.1 鼠標輸入
- 6.2 處理鍵盤事件
- 6.3 處理游戲手柄事件
- 第六章小結
- 第七章使用布局控件進行窗口布局
- 7.1 窗口布局基礎
- 7.2 窗口布局控件
- 7.3 使用布局控件進行編程
- 7.4 更多關于布局的話題
- 第七章小結
- 第八章使用標準對話框
- 8.1信息對話框
- 8.2 文件和目錄對話框
- 8.3 選擇和選項對話框
- 8.4 輸入對話框
- 8.5 打印對話框
- 第八章小結
- 第九章創建定制的對話框
- 9.1 創建定制對話框的步驟
- 9.2 一個例子:PersonalRecordDialog
- 9.3 在小型設備上調整你的對話框
- 9.4 一些更深入的話題
- 9.5 使用wxWidgets資源文件
- 第九章小結
- 第十章使用圖像編程
- 10.1 wxWidgets中圖片相關的類
- 10.2 使用wxBitmap編程
- 10.3 使用wxIcon編程
- 10.4 使用wxCursor編程
- 10.5 使用wxImage編程
- 10.6 圖片列表和圖標集
- 10.7 自定義wxWidgets提供的小圖片
- 第十章小結
- 第十一章剪貼板和拖放操作
- 11.1 數據對象
- 11.2 使用剪貼板
- 11.3 實現拖放操作
- 第十一章小結
- 第十二章高級窗口控件
- 12.1 wxTreeCtrl
- 12.2 wxListCtrl
- 12.3 wxWizard
- 12.4 wxHtmlWindow
- 12.5 wxGrid
- 12.6 wxTaskBarIcon
- 12.7 編寫自定義的控件
- 第十二章小結
- 第十三章數據結構類
- 13.1 為什么沒有使用STL?
- 13.2 字符串類型
- 13.3 wxArray
- 13.4 wxList和wxNode
- 13.5 wxHashMap
- 13.6 存儲和使用日期和時間
- 13.7 其它常用的數據類型
- 第十三章小結
- 第十四章文件和流操作
- 14.1 文件類和函數
- 14.2 流操作相關類
- 第十四章小結
- 第十五章內存管理,調試和錯誤處理
- 15.1 內存管理基礎
- 15.2 檢測內存泄漏和其它錯誤
- 15.3 構建自防御的程序
- 15.4 錯誤報告
- 15.5 提供運行期類型信息
- 15.6 使用wxModule
- 15.7 加載動態鏈接庫
- 15.8 異常處理
- 15.9 調試提示
- 第十五章小結
- 第十六章編寫國際化程序
- 16.1 國際化介紹
- 16.2 從翻譯說起
- 16.3 字符編碼和Unicode
- 16.4 數字和日期
- 16.5 其它媒介
- 16.6 一個小例子
- 第十六章小結
- 第十七章編寫多線程程序
- 17.1 什么時候使用多線程,什么時候不要使用
- 17.2 使用wxThread
- 17.3 用于線程同步的對象
- 17.4 多線程的替代方案
- 第十七章小結
- 第十八章使用wxSocket編程
- 18.1 Socket類和功能概覽
- 18.2 Socket及其基本處理介紹
- 18.3 Socket標記
- 18.4 使用Socket流
- 18.5 替代wxSocket
- 第十八章小結
- 第十九章使用文檔/視圖框架
- 19.1 文檔/視圖基礎
- 19.2 文檔/視圖框架的其它能力
- 19.3 實現Undo/Redo的策略
- 第十九章小結
- 第二十章完善你的應用程序
- 20.1 單個實例和多個實例
- 20.2 更改事件處理機制
- 20.3 降低閃爍
- 20.4 實現聯機幫助
- 20.5 解析命令行參數
- 20.6 存儲應用程序資源
- 20.7 調用別的應用程序
- 20.8 管理應用程序設置
- 20.9 應用程序安裝
- 20.10 遵循用戶界面設計規范
- 20.11 全書小結