天天看點

MFC 多線程及線程同步

一、mfc對多線程程式設計的支援

  mfc中有兩類線程,分别稱之為工作者線程和使用者界面線程。二者的主要差別在于工作者線程沒有消息循環,而使用者界面線程有自己的消息隊列和消息循環。

  工作者線程沒有消息機制,通常用來執行背景計算和維護任務,如冗長的計算過程,列印機的背景列印等。使用者界面線程一般用于處理獨立于其他線程執行之外的使用者輸入,響應使用者及系統所産生的事件和消息等。但對于win32的api程式設計而言,這兩種線程是沒有差別的,它們都隻需線程的啟動位址即可啟動線程來執行任務。

  在mfc中,一般用全局函數afxbeginthread()來建立并初始化一個線程的運作,該函數有兩種重載形式,分别用于建立工作者線程和使用者界面線程。兩種重載函數原型和參數分别說明如下:

  (1) cwinthread* afxbeginthread(

        afx_threadproc pfnthreadproc,

            lpvoid pparam,

            int npriority = thread_priority_normal,

            unt nstacksize = 0,

            dword dwcreateflags = 0,

            lpsecurity_attributes lpsecurityattrs = null

           );//用于建立工作者線程

  pfnthreadproc:指向工作者線程的執行函數的指針,線程函數原型必須聲明如下:

  uint executingfunction(lpvoid pparam);

  請注意,executingfunction()應傳回一個uint類型的值,用以指明該函數結束的原因。一般情況下,傳回0表明執行成功。

pparam:      一個32位參數,執行函數将用某種方式解釋該值。它可以是數值,或是指向一個結構的指針,甚至可以被忽略;

npriority:     線程的優先級。如果為0,則線程與其父線程具有相同的優先級;

nstacksize:   線程為自己配置設定堆棧的大小,其機關為位元組。如果nstacksize被設為0,則線程的堆棧被設定成與父線程堆棧相同大小;

dwcreateflags:如果為0,則線程在建立後立刻開始執行。如果為create_suspend,則線程在建立後立刻被挂起;

lpsecurityattrs:線程的安全屬性指針,一般為null;

  (2) cwinthread* afxbeginthread(

       cruntimeclass* pthreadclass,

           ); 

  pthreadclass 是指向 cwinthread 的一個導出類的運作時類對象的指針,該導出類定義了被建立的使用者界面線程的啟動、退出等;其它參數的意義同形式1。使用函數的這個原型生成的線程也有消息機制,在以後的例子中我們将發現同主線程的機制幾乎一樣。

  下面我們對cwinthread類的資料成員及常用函數進行簡要說明。

m_hthread:     目前線程的句柄;

m_nthreadid:   目前線程的id;

m_pmainwnd: 指向應用程式主視窗的指針

  該函數中的dwcreateflags、nstacksize、lpsecurityattrs參數和api函數createthread中的對應參數有相同含義,該函數執行成功,傳回非0值,否則傳回0。

  一般情況下,調用afxbeginthread()來一次性地建立并啟動一個線程,但是也可以通過兩步法來建立線程:首先建立cwinthread類的一個對象,然後調用該對象的成員函數createthread()來啟動該線程。

  重載該函數以控制使用者界面線程執行個體的初始化。初始化成功則傳回非0值,否則傳回0。使用者界面線程經常重載該函數,工作者線程一般不使用initinstance()。

  線上程終結前重載該函數進行一些必要的清理工作。該函數傳回線程的退出碼,0表示執行成功,非0值用來辨別各種錯誤。同initinstance()成員函數一樣,該函數也隻适用于使用者界面線程。

二、mfc中線程同步

  在程式中使用多線程時,一般很少有多個線程能在其生命期内進行完全獨立的操作。更多的情況是一些線程進行某些處理操作,而其他的線程必須對其處理結果進行了解。正常情況下對這種處理結果的了解應當在其處理任務完成後進行。

  如果不采取适當的措施,其他線程往往會線上程處理任務結束前就去通路處理結果,這就很有可能得到有關處理結果的錯誤了解。例如,多個線程同時通路同一個全局變量,如果都是讀取操作,則不會出現問題。如果一個線程負責改變此變量的值,而其他線程負責同時讀取變量内容,則不能保證讀取到的資料是經過寫線程修改後的。

  為了確定讀線程讀取到的是經過修改的變量,就必須在向變量寫入資料時禁止其他線程對其的任何通路,直至指派過程結束後再解除對其他線程的通路限制。象這種保證線程能了解其他線程任務處理結束後的處理結果而采取的保護措施即為線程同步。

  線程的同步可分使用者模式的線程同步和核心對象的線程同步兩大類。使用者模式中線程的同步方法主要有原子通路和臨界區等方法。其特點是同步速度特别快,适合于對線程運作速度有嚴格要求的場合。

  核心對象的線程同步則主要由事件、等待定時器、信号量以及信号燈等核心對象構成。由于這種同步機制使用了核心對象,使用時必須将線程從使用者模式切換到核心模式,而這種轉換一般要耗費近千個cpu周期,是以同步速度較慢,但在适用性上卻要遠優于使用者模式的線程同步方式。

  

  1.臨界區

  臨界區(critical section)是一段獨占對某些共享資源通路的代碼,在任意時刻隻允許一個線程對共享資源進行通路。如果有多個線程試圖同時通路臨界區,那麼在有一個線程進入後其他所有試圖通路此臨界區的線程将被挂起,并一直持續到進入臨界區的線程離開。臨界區在被釋放後,其他線程可以繼續搶占,并以此達到用原子方式操作共享資源的目的。

  臨界區在使用時以critical_section結構對象保護共享資源,并分别用entercriticalsection()和leavecriticalsection()函數去辨別和釋放一個臨界區。所用到的critical_section結構對象必須經過initializecriticalsection()的初始化後才能使用,而且必須確定所有線程中的任何試圖通路此共享資源的代碼都處在此臨界區的保護之下。否則臨界區将不會起到應有的作用,共享資源依然有被破壞的可能。

critical_section g_cs;

// 臨界區結構對象

char g_carray[10];

// 共享資源

uint threadproc10(lpvoid pparam)

{

 entercriticalsection(&g_cs);  // 進入臨界區

 for (int i

=

0; i

< 10; i++)

// 對共享資源進行寫入操作

 {

  g_carray[i] =

'a';

  sleep(1);

 }

 leavecriticalsection(&g_cs);  // 離開臨界區

 return

0;

}

uint threadproc11(lpvoid pparam)

 

 entercriticalsection(&g_cs);

  g_carray[10

- i

- 1]

'b';

 leavecriticalsection(&g_cs);

……

void csample08view::oncriticalsection()

 initializecriticalsection(&g_cs); 

// 初始化臨界區

 afxbeginthread(threadproc10, null); // 啟動線程

 afxbeginthread(threadproc11, null);

 sleep(300);

 cstring sresult = cstring(g_carray);

 afxmessagebox(sresult);

在使用臨界區時,一般不允許其運作時間過長,隻要進入臨界區的線程還沒有離開,其他所有試圖進入此臨界區的線程都會被挂起而進入到等待狀态,并會在一定程度上影響。程式的運作性能。尤其需要注意的是不要将等待使用者輸入或是其他一些外界幹預的操作包含到臨界區。如果進入了臨界區卻一直沒有釋放,同樣也會引起其他線程的長時間等待。換句話說,在執行了entercriticalsection()語句進入臨界區後無論發生什麼,必須確定與之比對的leavecriticalsection()都能夠被執行到。可以通過添加結構化異常處理代碼來確定leavecriticalsection()語句的執行。雖然臨界區同步速度很快,但卻隻能用來同步本程序内的線程,而不可用來同步多個程序中的線程。

  mfc為臨界區提供有一個ccriticalsection類,使用該類進行線程同步處理是非常簡單的,隻需線上程函數中用ccriticalsection類成員函數lock()和unlock()标定出被保護代碼片段即可。對于上述代碼,可通過ccriticalsection類将其改寫如下:

ccriticalsection g_clscriticalsection;  //

mfc臨界區類對象

char g_carray[10];             

uint threadproc20(lpvoid pparam)

 g_clscriticalsection.lock();      // 進入臨界區

< 10; i++)       //

對共享資源進行寫入操作

 g_clscriticalsection.unlock();      // 離開臨界區

uint threadproc21(lpvoid pparam)

 g_clscriticalsection.lock();

 g_clscriticalsection.unlock();

void csample08view::oncriticalsectionmfc()

 afxbeginthread(threadproc20, null);

 afxbeginthread(threadproc21, null);

2.事件核心對象

  在前面講述線程通信時曾使用過事件核心對象來進行線程間的通信,除此之外,事件核心對象也可以通過通知操作的方式來保持線程的同步。對于前面那段使用臨界區保持線程同步的代碼可用事件對象的線程同步方法改寫如下:

handle hevent = null;             // 事件句柄

char g_carray[10];             //

共享資源

uint threadproc12(lpvoid pparam)

 waitforsingleobject(hevent, infinite);  // 等待事件置位

 setevent(hevent);              // 處理完成後即将事件對象置位

uint threadproc13(lpvoid pparam)

 waitforsingleobject(hevent, infinite);

 setevent(hevent);

void csample08view::onevent()

 hevent = createevent(null, false, false, null);     // 建立事件

 setevent(hevent);                      // 事件置位

 afxbeginthread(threadproc12, null);            // 啟動線程

 afxbeginthread(threadproc13, null);

在建立線程前,首先建立一個可以自動複位的事件核心對象hevent,而線程函數則通過waitforsingleobject()等待函數無限等待hevent的置位,隻有在事件置位時waitforsingleobject()才會傳回,被保護的代碼将得以執行。對于以自動複位方式建立的事件對象,在其置位後一被waitforsingleobject()等待到就會立即複位,也就是說在執行threadproc12()中的受保護代碼時,事件對象已經是複位狀态的,這時即使有threadproc13()對cpu的搶占,也會由于waitforsingleobject()沒有hevent的置位而不能繼續執行,也就沒有可能破壞受保護的共享資源。在threadproc12()中的處理完成後可以通過setevent()對hevent的置位而允許threadproc13()對共享資源g_carray的處理。這裡setevent()所起的作用可以看作是對某項特定任務完成的通知。

  使用臨界區隻能同步同一程序中的線程,而使用事件核心對象則可以對程序外的線程進行同步,其前提是得到對此事件對象的通路權。可以通過openevent()函數擷取得到,其函數原型為:

handle openevent(

 dword dwdesiredaccess, // 通路标志

 bool binherithandle, // 繼承标志

 lpctstr lpname // 指向事件對象名的指針

);

 如果事件對象已建立(在建立事件時需要指定事件名),函數将傳回指定事件的句柄。對于那些在建立事件時沒有指定事件名的事件核心對象,可以通過使用核心對象的繼承性或是調用duplicatehandle()函數來調用createevent()以獲得對指定事件對象的通路權。在擷取到通路權後所進行的同步操作與在同一個程序中所進行的線程同步操作是一樣的。

  如果需要在一個線程中等待多個事件,則用waitformultipleobjects()來等待。waitformultipleobjects()與waitforsingleobject()類似,同時監視位于句柄數組中的所有句柄。這些被監視對象的句柄享有平等的優先權,任何一個句柄都不可能比其他句柄具有更高的優先權。waitformultipleobjects()的函數原型為:

dword waitformultipleobjects(

 dword ncount, // 等待句柄數

 const handle *lphandles,

// 句柄數組首位址

 bool fwaitall, // 等待标志

 dword dwmilliseconds // 等待時間間隔

參數ncount指定了要等待的核心對象的數目,存放這些核心對象的數組由lphandles來指向。fwaitall對指定的這ncount個核心對象的兩種等待方式進行了指定,為true時當所有對象都被通知時函數才會傳回,為false則隻要其中任何一個得到通知就可以傳回。dwmilliseconds在這裡的作用與在waitforsingleobject()中的作用是完全一緻的。如果等待逾時,函數将傳回wait_timeout。如果傳回wait_object_0到wait_object_0+ncount-1中的某個值,則說明所有指定對象的狀态均為已通知狀态(當fwaitall為true時)或是用以減去wait_object_0而得到發生通知的對象的索引(當fwaitall為false時)。如果傳回值在wait_abandoned_0與wait_abandoned_0+ncount-1之間,則表示所有指定對象的狀态均為已通知,且其中至少有一個對象是被丢棄的互斥對象(當fwaitall為true時),或是用以減去wait_object_0表示一個等待正常結束的互斥對象的索引(當fwaitall為false時)。

下面給出的代碼主要展示了對waitformultipleobjects()函數的使用。通過對兩個事件核心對象的等待來控制線程任務的執行與中途退出:

handle hevents[2];                           // 存放事件句柄的數組

uint threadproc14(lpvoid pparam)

 dword dwret1 = waitformultipleobjects(2, hevents, false, infinite); //

等待開啟事件

 if (dwret1

== wait_object_0)                       // 如果開啟事件到達則線程開始執行任務

  afxmessagebox("線程開始工作!");

  while (true)

  {

   for (int i

< 10000; i++);

   

   dword dwret2 = waitformultipleobjects(2, hevents, false,

0);   // 在任務處理過程中等待結束事件

   if (dwret2

== wait_object_0

+

1)                   // 如果結束事件置位則立即終止任務的執行

    break;

  }

 afxmessagebox("線程退出!");

void csample08view::onstartevent()

  for (int i

< 2; i++)                      //

建立線程

  hevents[i] = createevent(null, false, false, null);

  afxbeginthread(threadproc14, null);                  // 開啟線程

  setevent(hevents[0]);                         

// 設定事件0(開啟事件)

void csample08view::onendevent()

  setevent(hevents[1]);                          // 設定事件1(結束事件)

mfc為事件相關處理也提供了一個cevent類,共包含有除構造函數外的4個成員函數pulseevent()、resetevent()、setevent()和unlock()。在功能上分别相當與win32 api的pulseevent()、resetevent()、setevent()和closehandle()等函數。而構造函數則履行了原createevent()函數建立事件對象的職責,其函數原型為:

  cevent(bool binitiallyown = false, bool bmanualreset = false, lpctstr lpszname = null, lpsecurity_attributes lpsaattribute = null );

  3.信号量核心對象

  信号量(semaphore)核心對象對線程的同步方式與前面幾種方法不同,它允許多個線程在同一時刻通路同一資源,但是需要限制在同一時刻通路此資源的最大線程數目。在用createsemaphore()建立信号量時即要同時指出允許的最大資源計數和目前可用資源計數。一般是将目前可用資源計數設定為最大資源計數,每增加一個線程對共享資源的通路,目前可用資源計數就會減1,隻要目前可用資源計數是大于0的,就可以發出信号量信号。但是目前可用計數減小到0時則說明目前占用資源的線程數已經達到了所允許的最大數目,不能在允許其他線程的進入,此時的信号量信号将無法發出。線程在處理完共享資源後,應在離開的同時通過releasesemaphore()函數将目前可用資源計數加1。在任何時候目前可用資源計數決不可能大于最大資源計數。

  使用信号量核心對象進行線程同步主要會用到createsemaphore()、opensemaphore()、releasesemaphore()、waitforsingleobject()和waitformultipleobjects()等函數。其中,createsemaphore()用來建立一個信号量核心對象,其函數原型為:

handle createsemaphore(

 lpsecurity_attributes lpsemaphoreattributes, // 安全屬性指針

 long linitialcount,               // 初始計數

 long lmaximumcount,               // 最大計數

 lpctstr lpname                  // 對象名指針

); 

參數lmaximumcount是一個有符号32位值,定義了允許的最大資源計數,最大取值不能超過4294967295。lpname參數可以為建立的信号量定義一個名字,由于其建立的是一個核心對象,是以在其他程序中可以通過該名字而得到此信号量。opensemaphore()函數即可用來根據信号量名打開在其他程序中建立的信号量,函數原型如下:

handle opensemaphore(

 dword dwdesiredaccess,   // 通路标志

 bool binherithandle,    // 繼承标志

 lpctstr lpname        // 信号量名

線上程離開對共享資源的處理時,必須通過releasesemaphore()來增加目前可用資源計數。否則将會出現目前正在處理共享資源的實際線程數并沒有達到要限制的數值,而其他線程卻因為目前可用資源計數為0而仍無法進入的情況。releasesemaphore()的函數原型為:

bool releasesemaphore(

 handle hsemaphore,    // 信号量句柄

 long lreleasecount,   // 計數遞增數量

 lplong lppreviouscount // 先前計數

 該函數将lreleasecount中的值添加給信号量的目前資源計數,一般将lreleasecount設定為1,如果需要也可以設定其他的值。waitforsingleobject()和waitformultipleobjects()主要用在試圖進入共享資源的線程函數入口處,主要用來判斷信号量的目前可用資源計數是否允許本線程的進入。隻有在目前可用資源計數值大于0時,被監視的信号量核心對象才會得到通知。

  信号量的使用特點使其更适用于對socket(套接字)程式中線程的同步。例如,網絡上的http伺服器要對同一時間内通路同一頁面的使用者數加以限制,這時可以為沒一個使用者對伺服器的頁面請求設定一個線程,而頁面則是待保護的共享資源,通過使用信号量對線程的同步作用可以確定在任一時刻無論有多少使用者對某一頁面進行通路,隻有不大于設定的最大使用者數目的線程能夠進行通路,而其他的通路企圖則被挂起,隻有在有使用者退出對此頁面的通路後才有可能進入。下面給出的示例代碼即展示了類似的處理過程:

handle hsemaphore;                 // 信号量對象句柄

uint threadproc15(lpvoid pparam)

 waitforsingleobject(hsemaphore, infinite);  // 試圖進入信号量關口

 afxmessagebox("線程一正在執行!");        //

線程任務處理

 releasesemaphore(hsemaphore, 1, null);    // 釋放信号量計數

uint threadproc16(lpvoid pparam)

 waitforsingleobject(hsemaphore, infinite);

 afxmessagebox("線程二正在執行!");

 releasesemaphore(hsemaphore, 1, null);

uint threadproc17(lpvoid pparam)

 afxmessagebox("線程三正在執行!");

void csample08view::onsemaphore()

 hsemaphore = createsemaphore(null,

2,

2, null);  // 建立信号量對象

 afxbeginthread(threadproc15, null);         // 開啟線程

 afxbeginthread(threadproc16, null);

 afxbeginthread(threadproc17, null);

 在mfc中,通過csemaphore類對信号量作了表述。該類隻具有一個構造函數,可以構造一個信号量對象,并對初始資源計數、最大資源計數、對象名和安全屬性等進行初始化,其原型如下:

  csemaphore( long linitialcount = 1, long lmaxcount = 1, lpctstr pstrname = null, lpsecurity_attributes lpsaattributes = null );

  在構造了csemaphore類對象後,任何一個通路受保護共享資源的線程都必須通過csemaphore從父類csyncobject類繼承得到的lock()和unlock()成員函數來通路或釋放csemaphore對象。與前面介紹的幾種通過mfc類保持線程同步的方法類似,通過csemaphore類也可以将前面的線程同步代碼進行改寫,這兩種使用信号量的線程同步方法無論是在實作原理上還是從實作結果上都是完全一緻的。下面給出經mfc改寫後的信号量線程同步代碼:

// mfc信号量類對象

csemaphore g_clssemaphore(2,

2);

uint threadproc24(lpvoid pparam)

 // 試圖進入信号量關口

 g_clssemaphore.lock();

 // 線程任務處理

 afxmessagebox("線程一正在執行!");

 // 釋放信号量計數

 g_clssemaphore.unlock();

uint threadproc25(lpvoid pparam)

uint threadproc26(lpvoid pparam)

void csample08view::onsemaphoremfc()

 // 開啟線程

 afxbeginthread(threadproc24, null);

 afxbeginthread(threadproc25, null);

 afxbeginthread(threadproc26, null);

4.互斥核心對象

互斥(mutex)是一種用途非常廣泛的核心對象。能夠保證多個線程對同一共享資源的互斥通路。同臨界區有些類似,隻有擁有互斥對象的線程才具有通路資源的權限,由于互斥對象隻有一個,是以就決定了任何情況下此共享資源都不會同時被多個線程所通路。目前占據資源的線程在任務處理完後應将擁有的互斥對象交出,以便其他線程在獲得後得以通路資源。與其他幾種核心對象不同,互斥對象在作業系統中擁有特殊代碼,并由作業系統來管理,作業系統甚至還允許其進行一些其他核心對象所不能進行的非正常操作。

以互斥核心對象來保持線程同步可能用到的函數主要有createmutex()、openmutex()、releasemutex()、waitforsingleobject()和waitformultipleobjects()等。在使用互斥對象前,首先要通過createmutex()或openmutex()建立或打開一個互斥對象。createmutex()函數原型為:

handle createmutex(

  lpsecurity_attributes lpmutexattributes, // 安全屬性指針

  bool binitialowner, // 初始擁有者

  lpctstr lpname // 互斥對象名

參數binitialowner主要用來控制互斥對象的初始狀态。一般多将其設定為false,以表明互斥對象在建立時并沒有為任何線程所占有。如果在建立互斥對象時指定了對象名,那麼可以在本程序其他地方或是在其他程序通過openmutex()函數得到此互斥對象的句柄。openmutex()函數原型為:

handle openmutex(

  dword dwdesiredaccess, // 通路标志

  bool binherithandle, // 繼承标志

當目前對資源具有通路權的線程不再需要通路此資源而要離開時,必須通過releasemutex()函數來釋放其擁有的互斥對象,其函數原型為:

bool releasemutex(handle hmutex);

其唯一的參數hmutex為待釋放的互斥對象句柄。至于waitforsingleobject()和waitformultipleobjects()等待函數在互斥對象保持線程同步中所起的作用與在其他核心對象中的作用是基本一緻的,也是等待互斥核心對象的通知。但是這裡需要特别指出的是:在互斥對象通知引起調用等待函數傳回時,等待函數的傳回值不再是通常的wait_object_0(對于waitforsingleobject()函數)或是在wait_object_0到wait_object_0+ncount-1之間的一個值(對于waitformultipleobjects()函數),而是将傳回一個wait_abandoned_0(對于waitforsingleobject()函數)或是在wait_abandoned_0到wait_abandoned_0+ncount-1之間的一個值(對于waitformultipleobjects()函數)。以此來表明線程正在等待的互斥對象由另外一個線程所擁有,而此線程卻在使用完共享資源前就已經終止。除此之外,使用互斥對象的方法在等待線程的可排程性上同使用其他幾種核心對象的方法也有所不同,其他核心對象在沒有得到通知時,受調用等待函數的作用,線程将會挂起,同時失去可排程性,而使用互斥的方法卻可以在等待的同時仍具有可排程性,這也正是互斥對象所能完成的非正常操作之一。

  在編寫程式時,互斥對象多用在對那些為多個線程所通路的記憶體塊的保護上,可以確定任何線程在處理此記憶體塊時都對其擁有可靠的獨占通路權。下面給出的示例代碼即通過互斥核心對象hmutex對共享記憶體快g_carray[]進行線程的獨占通路保護。下面給出實作代碼清單:

// 互斥對象

handle hmutex = null;

uint threadproc18(lpvoid pparam)

 // 等待互斥對象通知

 waitforsingleobject(hmutex, infinite);

 // 對共享資源進行寫入操作

 // 釋放互斥對象

 releasemutex(hmutex);

uint threadproc19(lpvoid pparam)

void csample08view::onmutex()

 // 建立互斥對象

 hmutex = createmutex(null, false, null);

 // 啟動線程

 afxbeginthread(threadproc18, null);

 afxbeginthread(threadproc19, null);

 // 等待計算完畢

 // 報告計算結果

互斥對象在mfc中通過cmutex類進行表述。使用cmutex類的方法非常簡單,在構造cmutex類對象的同時可以指明待查詢的互斥對象的名字,在構造函數傳回後即可通路此互斥變量。cmutex類也是隻含有構造函數這唯一的成員函數,當完成對互斥對象保護資源的通路後,可通過調用從父類csyncobject繼承的unlock()函數完成對互斥對象的釋放。cmutex類構造函數原型為:

cmutex( bool binitiallyown = false, lpctstr lpszname = null, lpsecurity_attributes lpsaattribute = null );

該類的适用範圍和實作原理與api方式建立的互斥核心對象是完全類似的,但要簡潔的多,下面給出就是對前面的示例代碼經cmutex類改寫後的程式實作清單:

cmutex g_clsmutex(false, null); // mfc互斥類對象

uint threadproc27(lpvoid pparam)

 g_clsmutex.lock();  // 等待互斥對象通知

< 10; i++) 

 g_clsmutex.unlock();  // 釋放互斥對象

uint threadproc28(lpvoid pparam)

 g_clsmutex.lock();

 g_clsmutex.unlock();

void csample08view::onmutexmfc()

 afxbeginthread(threadproc27, null);

 afxbeginthread(threadproc28, null);

繼續閱讀