Windows多線程多任務設計初步

來源:互聯網
上載者:User
前言:]當前流行的Windows作業系統,它能同時運行幾個程式(獨立啟動並執行程式又稱之為進程),對於同一個程式,它又可以分成若干個獨立的執行流,我們稱之為線程,線程提供了多任務處理的能力。用進程和線程的觀點來研究軟體是當今普遍採用的方法,進程和線程的概念的出現,對提高軟體的並行性有著重要的意義。現在的應用軟體無一不是多線程多任務處理,單線城的軟體是不可想象的。因此掌握多線程多任務設計方法對每個程式員都是必需要掌握的。本文針對多線程技術在應用中經常遇到的問題,如線程間的通訊、同步等,對它們分別進行探討。

  一、 理解線程

  要講解線程,不得不說一下進程,進程是應用程式的執行執行個體,每個進程是由私人的虛擬位址空間、代碼、資料和其它系統資源群組成。進程在運行時建立的資源隨著進程的終止而死亡。線程的基本思想很簡單,它是一個獨立的執行流,是進程內部的一個獨立的執行單元,相當於一個子程式,它對應Visual C++中的CwinThread類的對象。單獨一個執行程式運行時,預設的運行包含的一個主線程,主線程以函數地址的形式,如main或WinMain函數,提供者的啟動點,當主線程終止時,進程也隨之終止,但根據需要,應用程式又可以分解成許多獨立執行的線程,每個線程並行的運行在同一進程中。

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

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

  1. 線程的啟動

  建立一個使用介面執行緒,首先要從類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。

  2.線程的優先順序

  以下的CwinThread類的成員函數用於線程優先順序的操作:

int GetThreadPriority();
BOOL SetThradPriority()(int nPriority);

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

  3.線程的懸掛、恢複

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

  4.結束線程

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

////////////////////////////////////////////////////////////////
//////CtestView message handlers
/////Set to True to end thread
Bool bend=FALSE;//定義的全域變數,用於控制線程的運行
//The Thread Function
UINT ThreadFunction(LPVOID pParam)//線程函數
{
while(!bend)
{Beep(100,100);
Sleep(1000);
}
return 0;
}
CwinThread *pThread;
HWND hWnd;
/////////////////////////////////////////////////////////////
Void CtestView::OninitialUpdate()
{
hWnd=GetSafeHwnd();
pThread=AfxBeginThread(ThradFunction,hWnd);//啟動線程
pThread->m_bAutoDelete=FALSE;//線程為手動刪除
Cview::OnInitialUpdate();
}
////////////////////////////////////////////////////////////////
Void CtestView::OnDestroy()
{ bend=TRUE;//改變變數,線程結束
WaitForSingleObject(pThread->m_hThread,INFINITE);//等待線程結束
delete pThread;//刪除線程
Cview::OnDestroy();
}
  三、 線程之間的通訊

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

  1. 利用使用者定義的訊息通訊

  在Windows程式設計中,應用程式的每一個線程都擁有自己的訊息佇列,甚至背景工作執行緒也不例外,這樣一來,就使得線程之間利用訊息來傳遞資訊就變的非常簡單。首先使用者要定義一個使用者訊息,如下所示:#define WM_USERMSG WMUSER+100;在需要的時候,在一個線程中調用

::PostMessage((HWND)param,WM_USERMSG,0,0)

CwinThread::PostThradMessage()

來向另外一個線程發送這個訊息,上述函數的四個參數分別是訊息將要發送到的目的視窗的控制代碼、要發送的訊息標誌符、訊息的參數WPARAM和LPARAM。下面的代碼是對上節代碼的修改,修改後的結果是線上程結束時顯示一個對話方塊,提示線程結束:

UINT ThreadFunction(LPVOID pParam)
{
while(!bend)
{
Beep(100,100);
Sleep(1000);
}
::PostMessage(hWnd,WM_USERMSG,0,0);
return 0;
}
////////WM_USERMSG訊息的響應函數為OnThreadended(WPARAM wParam,LPARAM lParam)
LONG CTestView::OnThreadended(WPARAM wParam,LPARAM lParam)
{
AfxMessageBox("Thread ended.");
Retrun 0;
}

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

  2.用事件對象實現通訊

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

////////////////////////////////////////////////////////////////////
Cevent threadStart,threadEnd;
////////////////////////////////////////////////////////////////////
UINT ThreadFunction(LPVOID pParam)
{
::WaitForSingleObject(threadStart.m_hObject,INFINITE);
AfxMessageBox("Thread start.");
while(!bend)
{
Beep(100,100);
Sleep(1000);
Int result=::WaitforSingleObject(threadEnd.m_hObject,0);
//等待threadEnd事件有訊號,無訊號時線程在這裡懸停
If(result==Wait_OBJECT_0)
Bend=TRUE;
}
::PostMessage(hWnd,WM_USERMSG,0,0);
return 0;
}
/////////////////////////////////////////////////////////////
Void CtestView::OninitialUpdate()
{
hWnd=GetSafeHwnd();
threadStart.SetEvent();//threadStart事件有訊號
pThread=AfxBeginThread(ThreadFunction,hWnd);//啟動線程
pThread->m_bAutoDelete=FALSE;
Cview::OnInitialUpdate();
}
////////////////////////////////////////////////////////////////
Void CtestView::OnDestroy()
{ threadEnd.SetEvent();
WaitForSingleObject(pThread->m_hThread,INFINITE);
delete pThread;
Cview::OnDestroy();
}

運行這個程式,當關閉程式時,才顯示提示框,顯示"Thread ended"
  四、 線程之間的同步

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

  1. 臨界區

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

#include "afxmt.h"
int array[10],destarray[10];
CCriticalSection Section;
////////////////////////////////////////////////////////////////////////
UINT WriteThread(LPVOID param)
{Section.Lock();
for(int x=0;x<10;x++)
array[x]=x;
Section.Unlock();
}
UINT ReadThread(LPVOID param)
{
Section.Lock();
For(int x=0;x<10;x++)
Destarray[x]=array[x];
Section.Unlock();
}

上述代碼啟動並執行結果應該是Destarray數組中的元素分別為1-9,而不是雜亂無章的數,如果不使用同步,則不是這個結果,有興趣的讀者可以實驗一下。
  2. 互斥

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

#include "afxmt.h"
int array[10],destarray[10];
CMutex Section;

/////////////////////////////////////////////////////////////
UINT WriteThread(LPVOID param)
{ CsingleLock singlelock;
singlelock (&Section);
singlelock.Lock();
for(int x=0;x<10;x++)
array[x]=x;
singlelock.Unlock();
}
UINT ReadThread(LPVOID param)
{ CsingleLock singlelock;
singlelock (&Section);
singlelock.Lock();

For(int x=0;x<10;x++)
Destarray[x]=array[x];
singlelock.Unlock();

}

  3. 訊號量

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

/////////////////////////////////////////////////////////////////
Csemaphore *semaphore;
Semaphore=new Csemaphore(2,2);
HWND hWnd=GetSafeHwnd();
AfxBeginThread(threadProc1,hWnd);
AfxBeginThread(threadProc2,hWnd);
AfxBeginThread(threadProc3,hWnd);
//////////////////////////////////////////////////////////////////////
UINT ThreadProc1(LPVOID param)
{CsingleLock singelLock(semaphore);
singleLock.Lock();
Sleep(10000);
::MessageBox((HWND)param,"Thread1 had access","Thread1",MB_OK);
return 0;
}
UINT ThreadProc2(LPVOID param)
{CSingleLock singelLock(semaphore);
singleLock.Lock();
Sleep(10000);
::MessageBox((HWND)param,"Thread2 had access","Thread2",MB_OK);
return 0;
}
UINT ThreadProc3(LPVOID param)
{CsingleLock singelLock(semaphore);
singleLock.Lock();
Sleep(10000);
::MessageBox((HWND)param,"Thread3 had access","Thread3",MB_OK);
return 0;

  對複雜的應用程式來說,線程的應用給應用程式提供了高效、快速、安全的資料處理能力。本文講述了線程中經常遇到的問題,希望對讀者朋友有一定的協助。 
 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.