多執行緒程式設計基礎知識

發表於2017-12-05

當前流行的Windows作業系統能同時執行幾個程式(獨立執行的程式又稱之為程式),對於同一個程式,它又可以分成若干個獨立的執行流,我們稱之為執行緒,執行緒提供了多工處理的能力。用程式和執行緒的觀點來研究軟體是當今普遍採用的方法,程式和執行緒的概念的出現,對提高軟體的並行性有著重要的意義。現在的大型應用軟體無一不是多執行緒多工處理,單執行緒的軟體是不可想象的。因此掌握多執行緒多工設計方法對每個程式設計師都是必需要掌握的。本例項針對多執行緒技術在應用中經常遇到的問題,如執行緒間的通訊、同步等,分別進行探討,並利用多執行緒技術進行執行緒之間的通訊,實現了數字的簡單排序。

一、 實現方法

1、理解執行緒

要講解執行緒,不得不說一下程式,程式是應用程式的執行例項,每個程式是由私有的虛擬地址空間、程式碼、資料和其它系統資源組成。程式在執行時建立的資源隨著程式的終止而死亡。執行緒的基本思想很簡單,它是一個獨立的執行流,是程式內部的一個獨立的執行單元,相當於一個子程式,它對應於Visual C++中的CwinThread類物件。單獨一個執行程式執行時,預設地包含的一個主執行緒,主執行緒以函式地址的形式出現,提供程式的啟動點,如main()或WinMain()函式等。當主執行緒終止時,程式也隨之終止。根據實際需要,應用程式可以分解成許多獨立執行的執行緒,每個執行緒並行的執行在同一程式中。

一個程式中的所有執行緒都在該程式的虛擬地址空間中,使用該程式的全域性變數和系統資源。作業系統給每個執行緒分配不同的CPU時間片,在某一個時刻,CPU只執行一個時間片內的執行緒,多個時間片中的相應執行緒在CPU內輪流執行,由於每個時間片時間很短,所以對使用者來說,彷彿各個執行緒在計算機中是並行處理的。作業系統是根據執行緒的優先順序來安排CPU的時間,優先順序高的執行緒優先執行,優先順序低的執行緒則繼續等待。

執行緒被分為兩種:使用者介面執行緒和工作執行緒(又稱為後臺執行緒)。使用者介面執行緒通常用來處理使用者的輸入並響應各種事件和訊息,其實,應用程式的主執行執行緒CWinAPP物件就是一個使用者介面執行緒,當應用程式啟動時自動建立和啟動,同樣它的終止也意味著該程式的結束,程式終止。工作執行緒用來執行程式的後臺處理任務,比如計算、排程、對串列埠的讀寫操作等,它和使用者介面執行緒的區別是它不用從CWinThread類派生來建立,對它來說最重要的是如何實現工作執行緒任務的執行控制函式。工作執行緒和使用者介面執行緒啟動時要呼叫同一個函式的不同版本;最後需要讀者明白的是,一個程式中的所有執行緒共享它們父程式的變數,但同時每個執行緒可以擁有自己的變數。

2、執行緒的管理和操作

(一)執行緒的啟動

建立一個使用者介面執行緒,首先要從類CwinThread產生一個派生類,同時必須使用DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE來宣告和實現這個CwinThread派生類。第二步是根據需要過載該派生類的一些成員函式如:ExitInstance()、InitInstance()、OnIdle()、PreTranslateMessage()等函式。最後呼叫AfxBeginThread()函式的一個版本:CWinThread* AfxBeginThread( CRuntimeClass* pThreadClass, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL ) 啟動該使用者介面執行緒,其中第一個引數為指向定義的使用者介面執行緒類指標變數,第二個引數為執行緒的優先順序,第三個引數為執行緒所對應的堆疊大小,第四個引數為執行緒建立時的附加標誌,預設為正常狀態,如為CREATE_SUSPENDED則執行緒啟動後為掛起狀態。

對於工作執行緒來說,啟動一個執行緒,首先需要編寫一個希望與應用程式的其餘部分並行執行的函式如Fun1(),接著定義一個指向CwinThread物件的指標變數*pThread,呼叫AfxBeginThread(Fun1,param,priority)函式,返回值賦給pThread變數的同時一併啟動該執行緒來執行上面的Fun1()函式,其中Fun1是執行緒要執行的函式的名字,也既是上面所說的控制函式的名字,param是準備傳送給執行緒函式Fun1的任意32位值,priority則是定義該執行緒的優先順序別,它是預定義的常數,讀者可參考MSDN。

(二)執行緒的優先順序

以下的CwinThread類的成員函式用於執行緒優先順序的操作:

上述的二個函式分別用來獲取和設定執行緒的優先順序,這裡的優先順序,是相對於該執行緒所處的優先權層次而言的,處於同一優先權層次的執行緒,優先順序高的執行緒先執行;處於不同優先權層次上的執行緒,誰的優先權層次高,誰先執行。至於優先順序設定所需的常數,自己參考MSDN就可以了,要注意的是要想設定執行緒的優先順序,這個執行緒在建立時必須具有THREAD_SET_INFORMATION訪問許可權。對於執行緒的優先權層次的設定,CwinThread類沒有提供相應的函式,但是可以通過Win32 SDK函式GetPriorityClass()和SetPriorityClass()來實現。

(三)執行緒的懸掛和恢復

CWinThread類中包含了應用程式懸掛和恢復它所建立的執行緒的函式,其中SuspendThread()用來懸掛執行緒,暫停執行緒的執行;ResumeThread()用來恢復執行緒的執行。如果你對一個執行緒連續若干次執行SuspendThread(),則需要連續執行相應次的ResumeThread()來恢復執行緒的執行。

(四)結束執行緒

終止執行緒有三種途徑,執行緒可以在自身內部呼叫AfxEndThread()來終止自身的執行;可以線上程的外部呼叫BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode )來強行終止一個執行緒的執行,然後呼叫CloseHandle()函式釋放執行緒所佔用的堆疊;第三種方法是改變全域性變數,使執行緒的執行函式返回,則該執行緒終止。下面以第三種方法為例,給出部分程式碼:

3、執行緒之間的通訊

通常情況下,一個次級執行緒要為主執行緒完成某種特定型別的任務,這就隱含著表示在主執行緒和次級執行緒之間需要建立一個通訊的通道。一般情況下,有下面的幾種方法實現這種通訊任務:使用全域性變數(上一節的例子其實使用的就是這種方法)、使用事件物件、使用訊息。這裡我們主要介紹後兩種方法。

(一) 利用使用者定義的訊息通訊

在Windows程式設計中,應用程式的每一個執行緒都擁有自己的訊息佇列,甚至工作執行緒也不例外,這樣一來,就使得執行緒之間利用訊息來傳遞資訊就變的非常簡單。首先使用者要定義一個使用者訊息,如下所示:#define WM_USERMSG WMUSER+100;在需要的時候,在一個執行緒中呼叫::PostMessage((HWND)param,WM_USERMSG,0,0)或CwinThread::PostThradMessage()來向另外一個執行緒傳送這個訊息,上述函式的四個引數分別是訊息將要傳送到的目的視窗的控制程式碼、要傳送的訊息標誌符、訊息的引數WPARAM和LPARAM。下面的程式碼是對上節程式碼的修改,修改後的結果是線上程結束時顯示一個對話方塊,提示執行緒結束:

上面的例子是工作者執行緒向使用者介面執行緒傳送訊息,對於工作者執行緒,如果它的設計模式也是訊息驅動的,那麼呼叫者可以向它傳送初始化、退出、執行某種特定的處理等訊息,讓它在後臺完成。在控制函式中可以直接使用::GetMessage()這個SDK函式進行訊息分檢和處理,自己實現一個訊息迴圈。GetMessage()函式在判斷該執行緒的訊息佇列為空時,執行緒將系統分配給它的時間片讓給其它執行緒,不無效的佔用CPU的時間,如果訊息佇列不為空,就獲取這個訊息,判斷這個訊息的內容並進行相應的處理。

(二)用事件物件實現通訊

線上程之間傳遞訊號進行通訊比較複雜的方法是使用事件物件,用MFC的Cevent類的物件來表示。事件物件處於兩種狀態之一:有訊號和無訊號,執行緒可以監視處於有訊號狀態的事件,以便在適當的時候執行對事件的操作。上述例子程式碼修改如下:

執行這個程式,當關閉程式時,才顯示提示框,顯示”Thread ended”。

4、執行緒之間的同步

前面我們講過,各個執行緒可以訪問程式中的公共變數,所以使用多執行緒的過程中需要注意的問題是如何防止兩個或兩個以上的執行緒同時訪問同一個資料,以免破壞資料的完整性。保證各個執行緒可以在一起適當的協調工作稱為執行緒之間的同步。前面一節介紹的事件物件實際上就是一種同步形式。Visual C++中使用同步類來解決作業系統的並行性而引起的資料不安全的問題,MFC支援的七個多執行緒的同步類可以分成兩大類:同步物件(CsyncObject、Csemaphore、Cmutex、CcriticalSection和Cevent)和同步訪問物件(CmultiLock和CsingleLock)。本節主要介紹臨界區(critical section)、互斥(mutexe)、訊號量(semaphore),這些同步物件使各個執行緒協調工作,程式執行起來更安全。

(一) 臨界區

臨界區是保證在某一個時間只有一個執行緒可以訪問資料的方法。使用它的過程中,需要給各個執行緒提供一個共享的臨界區物件,無論哪個執行緒佔有臨界區物件,都可以訪問受到保護的資料,這時候其它的執行緒需要等待,直到該執行緒釋放臨界區物件為止,臨界區被釋放後,另外的執行緒可以強佔這個臨界區,以便訪問共享的資料。臨界區對應著一個CcriticalSection物件,當執行緒需要訪問保護資料時,呼叫臨界區物件的Lock()成員函式;當對保護資料的操作完成之後,呼叫臨界區物件的Unlock()成員函式釋放對臨界區物件的擁有權,以使另一個執行緒可以奪取臨界區物件並訪問受保護的資料。同時啟動兩個執行緒,它們對應的函式分別為WriteThread()和ReadThread(),用以對公共陣列組array[]操作,下面的程式碼說明了如何使用臨界區物件:

上述程式碼執行的結果應該是Destarray陣列中的元素分別為1-9,而不是雜亂無章的數,如果不使用同步,則不是這個結果,有興趣的讀者可以實驗一下。

(二)互斥

互斥與臨界區很相似,但是使用時相對複雜一些,它不僅可以在同一應用程式的執行緒間實現同步,還可以在不同的程式間實現同步,從而實現資源的安全共享。互斥與Cmutex類的物件相對應,使用互斥物件時,必須建立一個CSingleLock或CMultiLock物件,用於實際的訪問控制,因為這裡的例子只處理單個互斥,所以我們可以使用CSingleLock物件,該物件的Lock()函式用於佔有互斥,Unlock()用於釋放互斥。實現程式碼如下:

(三)訊號量

訊號量的用法和互斥的用法很相似,不同的是它可以同一時刻允許多個執行緒訪問同一個資源,建立一個訊號量需要用Csemaphore類宣告一個物件,一旦建立了一個訊號量物件,就可以用它來對資源的訪問技術。要實現計數處理,先建立一個CsingleLock或CmltiLock物件,然後用該物件的Lock()函式減少這個訊號量的計數值,Unlock()反之。下面的程式碼分別啟動三個執行緒,執行時同時顯示二個訊息框,然後10秒後第三個訊息框才得以顯示。

二、 程式設計步驟

  1. 啟動Visual C++6.0,生成一個32位的控制檯程式,將該程式命名為”sequence”
  2. 輸入要排續的數字,宣告四個子執行緒;
  3. 輸入程式碼,編譯執行程式。

三、 程式程式碼

主要用到的WINAPI執行緒控制函式,有關詳細說明請檢視MSDN;

執行緒建立函式:

對臨界資源控制的多執行緒控制的訊號函式:

以上四個函式的宣告必須適合作為一個執行緒函式的必要條件才可以使用CreateThread建立一個執行緒。

  1. 呼叫方法必須是__stdcall,即函式引數壓棧順序由右到左,而且由函式本身負責
    棧的恢復, C和C++預設是__cdecl, 所以要顯式宣告是__stdcall
  2. 返回值必須是unsigned long
  3. 引數必須是一個32位值,如一個指標值或long型別
  4. 如果函式是類成員函式,必須宣告為static函式,在CreateThread時函式指標有特殊的寫法。如下(函式是類CThreadTest的成員函式中):

    之所以要宣告為static是由於,該函式必須要獨立於物件例項來使用,即使沒有宣告例項也可以使用。

氣泡排序思想(升序,降序同理,後面的演算法一樣都是升序):從頭到尾對資料進行兩兩比較進行交換,小的放前大的放後。這樣一次下來,最大的元素就會被交換的最後,然後下一次
迴圈就不用對最後一個元素進行比較交換了,所以呢每一次比較交換的次數都比上一次迴圈的次數少一,這樣N次之後資料就變得升序排列了

選擇排序思想:每一次都從無序的資料中找出最小的元素,然後和前面已經有序的元素序列的後一個元素進行交換,這樣整個源序列就會分成兩部分,前面一部分是已經排好序的有序序列,後面一部分是無序的,用於選出最小的元素。迴圈N次之後,前面的有序序列加長到跟源序列一樣長,後面的無序部分長度變為0,排序就完成了。

堆排序思想:堆:資料元素從1到N排列成一棵二叉樹,而且這棵樹的每一個子樹的根都是該樹中的元素的最小或最大的元素這樣如果一個無序資料集合是一個堆那麼,根元素就是最小或最大的元素堆排序就是不斷對剩下的資料建堆,把最小或最大的元素析透出來。下面的演算法,就是從最後一個元素開始,依據一個節點比父節點數值大的原則對所有元素進行調整,這樣調整一次就形成一個堆,第一個元素就是最小的元素。然後再對剩下的無序資料再進行建堆,注意這時後面的無序資料元素的序數都要改變,如第一次建堆後,第二個元素就會變成堆的第一個元素。

插入排序思想:把源資料序列看成兩半,前面一半是有序的,後面一半是無序的,把無序的資料從頭到尾逐個逐個的插入到前面的有序資料中,使得有序的資料的個數不斷增大,同時無序的資料個數就越來越少,最後所有元素都會變得有序。

快速排序思想:快速排序是分治思想的一種應用,它先選取一個支點,然後把小於支點的元素交換到支點的前邊,把大於支點的元素交換到支點的右邊。然後再對支點左邊部分和右邊部分進行同樣的處理,這樣若干次之後,資料就會變得有序。下面的實現使用了遞迴建立兩個遊標:iLow,iHigh;iLow指向序列的第一個元素,iHigh指向最後一個先選第一個元素作為支點,並把它的值存貯在一個輔助變數裡。那麼第一個位置就變為空並可以放置其他的元素。 這樣從iHigh指向的元素開始向前移動遊標,iHigh查詢比支點小的元素,如果找到,則把它放置到空置了的位置(現在是第一個位置),然後iHigh遊標停止移動,這時iHigh指向的位置被空置,然後移動iLow遊標尋找比支點大的元素放置到iHigh指向的空置的位置,如此往復直到iLow與iHigh相等。最後使用遞迴對左右兩部分進行同樣處理

四、 小結

對複雜的應用程式來說,執行緒的應用給應用程式提供了高效、快速、安全的資料處理能力。本例項講述了執行緒處理中經常遇到的問題,希望對讀者朋友有一定的幫助,起到拋磚引玉的作用。

相關文章