(轉)剖析MFC多線程程序的同步機制---譯

原文鏈接:Synchronization in Multithreaded Applications with MFC

 

簡介

本文探討基本的同步概念,並實際動手幫助新手掌握多線程編程。本文的重點在各種同步技巧。

基本概念

在線程執行過程中,或多或少都需要彼此交互,這種交互行爲有多種形式和類型。例如,一個線程在執行完它被賦予的任務後,通知另一個線程任務已經完成。然後第二個線程做開始剩下的工作。

下述對象是用來支持同步的:

1)信號量

2)互斥鎖

3)關鍵區域

4)事件

每個對象都有不同的目的和用途,但基本目的都是支持同步。當然還有其他可以用來同步的對象,比如進程和線程對象。後兩者的使用由程序員決定,比如說判斷一個給定進程或線程是否執行完畢爲了使用進程和線程對象來進行同步,我們一般使用Wait*函數,在使用這些函數時,你應當知道一個概念,任何被作爲同步對象的內核對象(關鍵區域除外)都處於兩種狀態之一:通知狀態和未通知狀態。例如,進程和線程對象,當他們開始執行時處於未通知狀態,而當他們執行完畢時處於通知狀態,

爲了判斷一個給定進程或線程是否已經結束,我們必須判斷表示其的對象是否處於通知狀態,而要達到這樣的目的,我們需要使用Wait*函數。

Wait*函數

      下面是最簡單的Wait*函數:

 

 

參數hHandle表示待檢查其狀態(通知或者未通知)的對象,dwMilliseconds表示調用線程在被檢查對象進入其通知狀態前應該等待的時間。若對象處於通知狀態或指定時間過去了,這個函數返回控制權給調用線程。若dwMilliseconds設置爲INIFINITE(值爲-1),則調用線程會一直等待直到對象狀態變爲通知,這有可能使得調用線程永遠等待下去,導致餓死

      例如,檢查指定線程是否正在執行, dwMilliseconds設置爲0,是爲了讓調用線程馬上返回。

 

下一個Wait類函數類似上面的,但它帶的是一系列句柄,並且等待其中之一或全部進入已通知狀態。

 

參數nCount表示待檢查的句柄個數,lpHandles指向句柄數組,若fWaitAllTRUE,則等待所有的對象進入已通知狀態,若爲FALSE,則當任何一個對象進入已通知狀態時,函數返回。dwMilliseconds意義同上。

例如,下面代碼判斷哪個進程會先結束:

 

句柄數組中索引號爲index的對象進入已通知狀態時,函數返回WAIT_OBJECT_0 + 索引號。若fWaitAllTRUE,則當所有對象進入已通知狀態時,函數返回WAIT_OBJECT_0

      一個線程若調用一個Wait*函數,則它從用戶模式切換爲內核模式。這帶來的後果有好有壞。不好的是切換進入內核模式大概需要1000個時鐘週期,這消耗不算小。好的是當進入內核模式後,就不需要使用處理器,而是進入休眠態,不參與處理器的調度了。

      現在讓我們進入MFC,並看看它能爲我們做些什麼。這裏有兩個類封裝了對Wait*函數的調用: CSingleLockCMultiLock
 

同步對象

等價的C++

Events

CEvent

Critical sections

CCriticalSection

Mutexes

CMutex

Semaphores

CSemaphore

每個類都從一個類--CSyncObject繼承下來,此類最有用的成員是重載的HANDLE運算符,它返回指定同步對象的內在句柄。所有這些類都定義在<AfxMt.h>頭文件中。

事件

      一般來說,事件用於這樣的情形下:當指定的動作發生後,一個線程(或多個線程)纔開始執行其任務。例如,一個線程可能等待必需的數據收集完後纔開始將其保存到硬盤上。有兩種事件:手動重置型和自動重置型。通過使用事件,我們可以輕鬆地通知另一個線程特定的動作已經發生了。對於手動重置型事件,線程使用它通知多個線程特定動作已經發生,而對於自動重置型事件,線程使用它只可以通知一個線程。在MFC中,CEvent類封裝了事件對象(若在win32中,它是用一個HANDLE來表示的)。CEvent的構造函數運行我們選擇創建手動重置型和自動重置型事件。默認的創建類型是自動重置型事件。爲了通知正在等待的線程,我們可以調用CEvent::SetEvent方法,這個方法將會讓事件進入已通知狀態。若事件是手動重置型,則事件會保持已通知狀態,直到對應的CEvent::ResetEvent被調用,這個方法將使得事件進入未通知狀態。這個特性使得一個線程可以通過一個SetEvent調用去通知多個線程。若事件是自動重置型,則所有正在等待的線程中只有一個線程會接收到通知。當那個線程接收到通知後,事件會自動進入未通知狀態。

      下面兩個例子將展示上述特性:

 

在這個例子中,一個全局的CEvent對象被創建,當然它是自動重置型的。除此以外,有兩個工作線程在等待這個事件對象以便開始其工作。只要第三個線程調用那個事件對象的SetEvent方法,則兩個線程中之一(當然沒人知道會是哪個)會接收到通知,然後事件會進入未通知狀態,這就防止了第二個線程也得到事件的通知。

      下面來看第二個例子:


    這段代碼和上面的稍有不同,CEvent對象構造函數的參數不一樣了,但意義上就大不同了,這是一個手動重置型事件對象。若第三個線程調用事件對象的SetEvent方法,則可以確保兩個工作線程都會同時(幾乎是同時)開始工作。這是因爲手動重置型事件在進入已通知狀態後,會保持此狀態直到對應的ResetEvent被調用。

      除此以外事件對象還有一個方法:CEvent::PulseEvent。這個方法首先使得事件對象進入已通知狀態,然後使其退回到未通知狀態。若事件是手動重置型,事件進入已通知狀態會讓所有正在等待的線程得到通知,然後事件進入未通知狀態。若事件是自動重置型,事件進入已通知狀態時只會讓所有等待的線程之一得到通知。若沒有線程在等待,則調用ResetEvent什麼也不幹。

實例---工作者線程

       本文所帶的例子中,作者將展示如何創建工作者線程以及如何合理地銷燬它們。作者定義了一個被所有線程使用的控制函數。當點擊視圖區域時,就創建一個線程。所有被創建的線程使用上述控制函數在視圖客戶區繪製一個運動的圓形。這裏作者使用了一個手動重置型事件,它被用來通知所有工作線程其死訊。除此以外,我們將看到如何使得主線程等待直到所有工作者線程銷燬掉。

作者將線程函數定義爲全局的:


        注意作者傳入的是一個安全句柄,而不是一個CWnd指針,並且在線程函數中通過傳入的句柄創建一個臨時的C++對象並使用。這樣就避免了在多線程編程中多個對象引用單個C++對象的危險。


    爲了合理地銷燬所有線程,首先使得事件進入已通知狀態,這會通知工作線程“死期已至”,然後調用WaitForSingleObject讓主線程等待所有的工作者線程完全銷燬掉。注意每次迭代時調用WaitForSingleObject會導致從用戶模式進入內核模式。例如,10此迭代會浪費掉大約10000次時鐘週期。爲了避免這個問題,我們可以使用WaitForMultipleObjects。這就是第二種方法。


 關鍵區域

      和其他同步對象不同,除非有需要以外,關鍵區域工作在用戶模式下。若一個線程想運行一個封裝在關鍵區域中的代碼,它首先做一個旋轉封鎖,然後等待特定的時間,它進入內核模式去等待關鍵區域。實際上,關鍵區域持有一個旋轉計數器和一個信號量,前者用於用戶模式的等待,後者用於內核模式的等待(休眠態)。在Win32API中,有一個CRITICAL_SECTION結構體表示關鍵區域對象。在MFC中,有一個類CCriticalSection。關鍵區域是這樣一段代碼,當它被一個線程執行時,必須確保不會被另一個線程中斷。

一個簡單的例子是多個線程共用一個全局變量:



      這段代碼不是線程安全的,因爲沒有線程對變量g_nVariable是獨佔使用的。爲了解決這個問題,可以如下使用:

 

    這裏使用了CCriticalSection類的兩個方法,調用Lock函數通知系統下面代碼的執行不能被中斷,直到相同的線程調用Unlock方法。系統會首先檢查被系統關鍵區域封鎖的代碼是否被另一個線程捕獲。若是,則線程等待直到捕獲線程釋放掉關鍵區域。

      若有多個共享資源需要保護,則最好爲每個資源使用一個單獨的關鍵區域。記得要配對使用UnLockLock。還有一點是需要防止死鎖


 

互斥鎖

      和關鍵區域類似,互斥鎖設計爲對同步訪問共享資源進行保護。互斥鎖在內核中實現,因此需要進入內核模式操縱它們。互斥鎖不僅能在不同線程之間,也可以在不同進程之間進程同步。要跨進程使用,則互斥鎖應該是有名的。MFC中使用CMutex類來操縱互斥鎖。可以如下方式使用:



我們可以使用互斥鎖來限制應用程序的運行實例爲一個。可以將如下代碼放置到InitInstance函數(或WinMain)中:


信號量

      爲了限制使用共享資源的線程數目,我們應該使用信號量。信號量是一個內核對象。它存儲了一個計數器變量來跟蹤使用共享資源的線程數目。例如,下面代碼使用CSemaphore類創建了一個信號量對象,它確保在給定的時間間隔內(由構造函數第一個參數指定)最多隻有5個線程能使用共享資源。還假定初始時沒有線程獲得資源:

 

一旦線程訪問共享資源,信號量的計數器就減1.若變爲0,則接下來對資源的訪問會被拒絕,直到有一個持有資源的線程離開(也就是說釋放了信號量)。我們可以如下使用:

 


 主從線程之間的通信

      若主線程想通知從線程一些動作的發生,使用事件對象是很方便的。但反過來卻是低效,不方便的。因爲這會讓主線程停下來等待事件,進而降低了應用程序的響應速度。作者提出的方法是讓從線程發自定義消息給父線程。

 

    這隻能保證窗口類中唯一,但爲了確保整個應用程序中唯一,更爲安全的方式是:

 

 

    但這個方法有個很大的缺陷--內存泄露,作者沒有深入研究,可以參考我這篇文章《淺談一個線程通信代碼的內存泄露及解決方案 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章