MFC中線程相關知識


MFC中把線程分為兩種類型,UI線程和工作者線程。

MFC中啟動一個線程的最好方法是調用AfxBeginThread,有兩個版本,一個用於啟動Ui線程,另外一個用於啟動工作者線程。在MFC程序中,只有在線程不使用MFC庫時,才可以使用Win32的CreateThread函數來創建線程。AfxBeginThread不僅僅是對CreateThread函數的封裝,它還會初始化主結構使用的內部狀態信息,在不同的地方執行合理的檢查,確保以線程安全的方式訪問運行時庫中的函數。

工作線程的創建

調用AfxBeginThread函數,它將創建一個新的CwinThread對象,啟動一個線程並返回一個CwinThread對象,該對象擁有這個線程。

CwinThread * pThread = AfxBeginThread(ThreadFunc, &threadInfo);

ThreadFunc(LPVOID pParam)

{}

ThreadFunc是線程函數,threadInfo是用於包含線程的輸入。

 

CWinThread* AfxBeginThread(

   AFX_THREADPROC pfnThreadProc,

   LPVOID pParam,

   int nPriority = THREAD_PRIORITY_NORMAL,

   UINT nStackSize = 0,

   DWORD dwCreateFlags = 0,

   LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL

);

nPriority:指定線程的執行優先級別,是相對於該線程所屬的進程優先級別而定的。

nStackSize:指定了線程最大的堆棧的尺寸,默認值0允許堆棧增加到1MB大小,限制1MB,幾乎對所有的應用程序都比較適合。

dwCreateFlags:默認值0告訴系統立即開始執行線程,如果指定了CREATE_SUSPENDED,則線程開始時處於暫停狀態,直到調用者執行pThread->ResumeThread()函數。

lpSecurityAttrs:該結構指定了新線程的安全屬性,默認值NULL意味着新線程與創建它的線程具有相同的屬性。

 

線程函數是回調函數,因此它必須是靜態函數或是在類外部聲明的全局函數,            

UNIT ThreadFunc(LPVOID pParam);

pParam是一個32位值,等於傳遞給AfxBeginThread的pParam。

 

UI線程的創建

工作者線程是由其線程函數定義的,而UI線程的行為卻是由CwinThread派生來的可動態創建類控制的,該類與CwinApp派生的應用程序類很類似。

下面給出的UI線程類創建一個框架窗口,並調用AfxBeginThread函數啟動CUIThread。

//聲明一個CwinThread類

class CUIThread : public CWinThread

{

         DECLARE_DYNCREATE(CUIThread)

 

protected:

         CUIThread();           // 動態創建所使用的受保護的構造函數

         virtual ~CUIThread();

 

public:

         virtual BOOL InitInstance();

         virtual int ExitInstance();

 

protected:

         DECLARE_MESSAGE_MAP()

};

 

BOOL CUIThread::InitInstance()

{

         // TODO:    在此執行任意逐線程初始化

         m_pMainWnd = new MyWindow();

         m_pMainWnd->ShowWindow(SW_SHOW);

         m_pMainWnd->UpdateWindow();

         return TRUE;

}

 

//window class

class MyWindow : public CFrameWnd

{

         DECLARE_DYNCREATE(MyWindow)

public:

         MyWindow();           // 動態創建所使用的受保護的構造函數

         virtual ~MyWindow();

protected:

         DECLARE_MESSAGE_MAP()

};

 

IMPLEMENT_DYNCREATE(MyWindow, CFrameWnd)

MyWindow::MyWindow(){

         Create(NULL, _T("UI Thread Window"));

}

MyWindow::~MyWindow()

{}

BEGIN_MESSAGE_MAP(MyWindow, CFrameWnd)

END_MESSAGE_MAP()

通過AfxBeginThread(RUNTIME_CLASS(CUIThread));函數,啟動UI線程。AfxBeginThread的UI線程版本接收與工作線程版本中相同的4個可選參數,但是不接收pParam參數,一旦啟動,UI線程就會與創建它的線程異步運行。

 

線程的其他操作和信息

調用CwinThread::SuspendThread可以讓運行的線程掛起ResumeThread則讓掛起的線程繼續執行。對於每個線程,系統都維持一個“計數器”,suspendThread加1,resumeThread減1,只有計數器為0時,才會給線程調度處理時間。

 

API函數::Sleep可以讓當前線程掛起指定的時間,::sleep還可以用來放棄剩余的線程時間片。

::sleep(0);暫停當前線程並允許調度程序運行其他相同或更高的優先級別的線程,如果沒有優先級相同或者更高的線程處於等待狀態,函數調用會立即返回並繼續執行當前的線程。注意:sleep指定的掛起時間並不精確。

 

當工作線程的線程函數執行到return,或者線程中調用AfxEndThread函數時,工作者線程結束。當給消息隊列發送WM_QUIT或是調用了AfxEndThread時,UI線程就會結束。AfxEndThread、::PostQuitmessage、return都接收一個 32位的出口代碼,可以使用::GetExitCodeThread檢索得到線程的退出碼。

 

MFC會在線程結束后通過指針調用delete,CwinThread的析構函數會使用::closeHandleAPI函數來關閉線程句柄。線程句柄必須以顯示的方式被關閉,因為即使與句柄相關的線程終止了,但是句柄仍然打開着。它們必須保持為打開狀態,否則::GetExitCodeThread函數就不能正常工作了。所以當CwinThread結束后,再調用GetExitCodeThread函數獲得退出碼,這是很危險的事情,因為這時候的線程句柄已經被關閉。

 

我們可以CWinThread對象的m_bAutoDelete數據成員為FALSE可防止MFC刪除CwinThread對象。默認值是TRUE允許自動刪除。這時我們需要手動調用delete。這時創建線程時,需要使它處於暫停狀態。

 

MFC中關於跨線程界限調用MFC成員函數的問題

如果線程A給線程B傳遞了一個CWnd指針,而線程B要調用CWnd對象的成員函數,那么MFC在調試狀態下可能就會出現斷言錯誤。

首先,我們可能有這樣的錯覺,很多MFC成員函數都能夠在其他線程創建的對象中得到使用。那是因為這些函數大多數是內聯函數,它們僅僅是API函數的簡單封裝。

例如:GetParent函數的原型

AFXWIN_INLINE CWnd* CWnd::GetParent()const{

   ASSERT(::IsWindow(m_hWnd));

   Return CWnd::Fromhandle(::GetParent(m_hWnd));

}

這里的m_hWnd毫無疑問是有效,因為m_hWnd是CWnd的成員對象,但是我們如果調用GetParentFrame函數,則會出現斷言錯誤,造成錯誤的一條語句是:

ASSERT_VALID(this);該語句會執行有效的檢查,來確保與this關聯的HWND出現在了主結構用來將HWND轉換為CWnd的映射表中,該表僅對本線程可見,因此其他線程創建的CWnd對象和他的映射表在本線程中是查不到的,所以會出現斷言的錯誤。當然我們可以人為的給映射表添加上我們新增的HWND。我們將句柄通過線程傳入,在線程函數中使用,FromHandle((HWND)pParam),這樣就可以了。這就是為什么窗口、GDI對象、其他線程對象應盡量使用句柄而不是指針在線程中傳遞的原因。

那些沒有封裝HWND、HDC或其他句柄類型的類,也無法確保其正常工作。總之,在實際工作中,多線程MFC程序趨向於將大量的用戶界面工作交給主線程來執行,后台線程想要更新用戶界面,將消息發送給主線程,讓主線程更新即可。

 

線程同步

Windows支持4中類型的同步對象,可以用來同步由並發運行的線程所執行的操作:

1 臨界區

2 互斥量

3 事件

4 信號量

MFC在名為CcriticalSection、CMutex、CEvent和CSemaphone中的類封裝了這些對象,除此以外還包括CSingleLock和CMultiLock類。

 

臨界區

臨界區是最簡單的線程同步對象,它用來在同一個進程中,對兩個或者多個線程共享資源進行串行化訪問。基本思想是,每個獨占性地訪問一個資源的線程可以在訪問那個資源之前鎖定臨界區,訪問完成后解除鎖定。如果有線程B試圖鎖定線程A鎖定的臨界區,那么線程B將阻塞知道該臨界區空閑。使用CcriticalSection的lock函數可以鎖定臨界區,unlock函數則解除對臨界區的鎖定,如下展示了線程A和線程B如何串行化訪問ListA的。

// Global data

CCriticalSection g_cs;

...

// Thread A

g_cs.Lock();

// write to List A

g_cs.UnLock();

 

// Thread B

g_cs.Lock();

// read from List A

g_cs.UnLock();

 

 

互斥量

和臨界區一樣,互斥量也是用來獲得對兩個或者多個線程建共享資源的獨占性訪問,但是Mutex的作用范圍更大,不僅能同步同一進程內的線程,還能同步不同進程上的線程,因此開銷比使用臨界區大。通常情況下,同一進程內的同步我們使用臨界區,而不同進程間的線程同步我們使用互斥量。

假定兩個應用程序使用一塊共享內存來交換數據,則該共享數據內部必須防止並發線程訪問。

// Global data

CMutex g_mutex(FALSE, _T("MyMutex"));

...

g_mutex.Lock();

//Read or Wrtie the data

g_mutex.UnLock();

傳遞給CMutex構造函數的第一個參數指定互斥量的初始狀態是鎖定(TRUE)還是沒有鎖定(FALSE),第二個參數用來指定互斥量的名稱,如果你要同步不同進程間的線程,請確保互斥量的名稱相同,這樣才能引用Windows內核中相同的互斥量對象。

此外,如果一個線程鎖定了臨界區,當線程結束時,如果沒有釋放臨界區,則該臨界區將永遠被鎖定,其他想訪問該臨界區的線程只能無限期阻塞。但是,如果一個線程獲得了鎖之后,線程結束時沒有釋放鎖,那么系統將人為該鎖被拋棄了,釋放該鎖,這樣其他等待線程將得到該鎖,並繼續執行。

 

事件

MFC的CEvent類封裝了Win32事件對象,一個事件不只是操作系統內核中的一個標記,在特定的事件,事件只有兩個狀態中的一種,要么是處於信號狀態(設置)要么就是非信號狀態(重置)。

Windows支持兩種不同類型的事件:自動重置事件和手動重置事件。當在自動重置事件上阻塞的線程被喚醒時,該事件被自動重置為非信號狀態。而手動重置事件不能自動重置,它必須使用編程的方式重置。一般自動重置事件和手動重置事件的一般使用規則如下:      

如果事件只觸發一個線程,那么使用自動重置事件和使用SetEvent來喚醒等待線程即可,這里我們不需要使用ResetEvent函數,因為線程被喚醒的那一刻,事件將被自動重置。

如果事件將要觸發兩個或者多個線程,那么使用手動重置事件和PulseEvent喚醒所有的等待線程,也無需使用ResetEvent,因為PulseEvent在喚醒線程后將為您重置事件。

PulseEvent函數不僅能夠設置和重置事件,還確保了所有在事件上等待的線程在重置事件之前被喚醒,而SetEvent和ResetEvent顯然無法保證能夠做到這一點。
CEvent類構造函數原型:
CEvent(

    BOOL bInitiallyOwn = FALSE, 

    BOOL bManualReset = FALSE, 

    LPCTSTR lpszName = NULL, 

    LPSECURITY_ATTRIBUTES lpsaAttribute = NULL);

bInitiallyOwn:指定對象被初始化為信號狀態(True)還是無信號狀態(False)

bManualReset:指定對象是手動重置事件(True)還是自動重置事件(False)

lpszName:指定了事件對象的名稱,和mutex一樣,如果事件用來同步不同進程間的線程,必須給同步事件指定一個名字。如果事件是用來同步同一個進程內的線程的,則該值應該指定為NULL。

lpsaAttribute:描述事件對象的安全屬性,NULL表示接受默認值,適用於大部分應用程序。

事件對象使用Lock函數來阻塞當前線程,等待事件被Set。

注意:對於自動重置事件對象來說,只要該對象被使用,就會重置。比如此時一個自動重置事件對象g_event有信號狀態,調用檢查事件的函數::WaitForSingleObject(g_event.m_hObject,0),

這時,事件的狀態會被自動重置。

 

信號量

臨界區、互斥量和事件,具有這樣的共性,只能表示兩種狀態,但是信號量不同,它始終保存有代表可用資源數量的資源數。鎖定信號量會減少資源數,釋放信號量會增加資源數,只有線程試圖鎖定資源數為0的信號量時,線程才會被阻塞。直到其他線程釋放信號量,資源數增加或者超時時間期滿,該線程才被釋放。信號量可以用來同步化同一進程中的線程也可以同步化不同進程中的線程。Csemaphore的構造函數如下:
CSemaphore(

    LONG lInitialCount = 1, 

    LONG lMaxCount = 1, 

    LPCTSTR pstrName = NULL, 

    LPSECURITY_ATTRIBUTES lpsaAttributes = NULL);

lInitialCount:初始可訪問的資源數

lMaxCount:最大可訪問資源數

pstrName和lpsaAttributes同事件一致。

我們使用Lock函數鎖定資源,使用Unlock函數釋放資源。Unlock函數等價的API函數為::ReleaseSemaphore函數。信號量的傳統用法是,它允許一組線程(m個)共享n個資源,其中m比n大。

 

CsingLockCmultiLock

可以使用CsingLock對象來包裝臨界區、互斥對象、事件和信號量。那為什么需要去額外的包裝這么一層呢?考慮如下代碼,如果調用lock和unlock直接發生異常時,那么臨界區將用於被鎖定。

CcriticalSection g_cs;//全局變量

 

g_cs.Lock();

......

g_cs.UnLock();

但是如果我們使用CsingleLock的話,就不存在這個問題了。

 

CsingleLock lock(&g_cs);

lock.Lock();

......

lock.UnLock();

因為CsingleLock對象在堆棧上創建時,如果異常就會調用它的析構函數,析構函數會調用Unlock函數。

CmultiLock函數則完全不同,通過使用CmultiLock函數,一個線程可以一次阻塞最多64個同步化對象,並且根據Lock方式的不同,按不同條件從阻塞狀態中返回。注意:只有事件、互斥對象和信號量可以封裝在CmultiLock對象中,而臨界區不行。

Cmutex g_mutex;

CEvent g_event[2];

CsyncObject * g_pObjects[3] = {&g_mutex, &g_event[0], &g_event[1]};

....

CmultiLock  multiLock(g_pObject,3);

multiLock.Lock();//當前線程阻塞,直到三個對象都有信號狀態或者釋放鎖為止。

....

multiLock.Lock(INFINITE, FALSE); //當前線程阻塞,直到三個對象中有一個都有信號狀態或者釋放鎖為止。

Lock函數原型:

DWORD Lock(

    DWORD dwTimeOut = INFINITE, 

    BOOL bWaitForAll = TRUE, 

DWORD dwWakeMask = 0);

dwTimeOut:指定等待時間,默認為無限期等待。

bWaitForAll:指定是否等待所有同步對象解鎖(TRUE)還是只要有一個解鎖(FALSE)。

dwWakeMask:掩碼,指定喚醒線程的其他條件,例如WM_PAINT或是鼠標消息等。

 

關於Windows中多任務和多線程的小知識點

關於進程的幾個API函數:

CreateProcess:創建進程

GetExitCodeProcess:獲得進程的退出碼

WaitForInputIdle函數:等到創建的進程窗口開始處理消息並清空它的消息隊列。

WaitForMultipleObject:該函數是Win32中CmulitLock::Lock的等價函數,能夠監視多個對象。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM