當前流行的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類的成員函式用於執行緒優先順序的操作:
1 2 |
int GetThreadPriority(); BOOL SetThradPriority()(int nPriority); |
上述的二個函式分別用來獲取和設定執行緒的優先順序,這裡的優先順序,是相對於該執行緒所處的優先權層次而言的,處於同一優先權層次的執行緒,優先順序高的執行緒先執行;處於不同優先權層次上的執行緒,誰的優先權層次高,誰先執行。至於優先順序設定所需的常數,自己參考MSDN就可以了,要注意的是要想設定執行緒的優先順序,這個執行緒在建立時必須具有THREAD_SET_INFORMATION訪問許可權。對於執行緒的優先權層次的設定,CwinThread類沒有提供相應的函式,但是可以通過Win32 SDK函式GetPriorityClass()和SetPriorityClass()來實現。
(三)執行緒的懸掛和恢復
CWinThread類中包含了應用程式懸掛和恢復它所建立的執行緒的函式,其中SuspendThread()用來懸掛執行緒,暫停執行緒的執行;ResumeThread()用來恢復執行緒的執行。如果你對一個執行緒連續若干次執行SuspendThread(),則需要連續執行相應次的ResumeThread()來恢復執行緒的執行。
(四)結束執行緒
終止執行緒有三種途徑,執行緒可以在自身內部呼叫AfxEndThread()來終止自身的執行;可以線上程的外部呼叫BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode )來強行終止一個執行緒的執行,然後呼叫CloseHandle()函式釋放執行緒所佔用的堆疊;第三種方法是改變全域性變數,使執行緒的執行函式返回,則該執行緒終止。下面以第三種方法為例,給出部分程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//////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; } |
1 2 3 4 5 6 7 8 9 |
CwinThread *pThread; HWND hWnd; Void CtestView::OninitialUpdate() { hWnd=GetSafeHwnd(); pThread=AfxBeginThread(ThradFunction,hWnd);//啟動執行緒 pThread->m_bAutoDelete=FALSE;//執行緒為手動刪除 Cview::OnInitialUpdate(); } |
1 2 3 4 5 6 7 |
Void CtestView::OnDestroy() { bend=TRUE;//改變變數,執行緒結束 WaitForSingleObject(pThread->m_hThread,INFINITE);//等待執行緒結束 delete pThread;//刪除執行緒 Cview::OnDestroy(); } |
3、執行緒之間的通訊
通常情況下,一個次級執行緒要為主執行緒完成某種特定型別的任務,這就隱含著表示在主執行緒和次級執行緒之間需要建立一個通訊的通道。一般情況下,有下面的幾種方法實現這種通訊任務:使用全域性變數(上一節的例子其實使用的就是這種方法)、使用事件物件、使用訊息。這裡我們主要介紹後兩種方法。
(一) 利用使用者定義的訊息通訊
在Windows程式設計中,應用程式的每一個執行緒都擁有自己的訊息佇列,甚至工作執行緒也不例外,這樣一來,就使得執行緒之間利用訊息來傳遞資訊就變的非常簡單。首先使用者要定義一個使用者訊息,如下所示:#define WM_USERMSG WMUSER+100;在需要的時候,在一個執行緒中呼叫::PostMessage((HWND)param,WM_USERMSG,0,0)或CwinThread::PostThradMessage()來向另外一個執行緒傳送這個訊息,上述函式的四個引數分別是訊息將要傳送到的目的視窗的控制程式碼、要傳送的訊息標誌符、訊息的引數WPARAM和LPARAM。下面的程式碼是對上節程式碼的修改,修改後的結果是線上程結束時顯示一個對話方塊,提示執行緒結束:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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的時間,如果訊息佇列不為空,就獲取這個訊息,判斷這個訊息的內容並進行相應的處理。
(二)用事件物件實現通訊
線上程之間傳遞訊號進行通訊比較複雜的方法是使用事件物件,用MFC的Cevent類的物件來表示。事件物件處於兩種狀態之一:有訊號和無訊號,執行緒可以監視處於有訊號狀態的事件,以便在適當的時候執行對事件的操作。上述例子程式碼修改如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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; } |
1 2 3 4 5 6 7 8 |
Void CtestView::OninitialUpdate() { hWnd=GetSafeHwnd(); threadStart.SetEvent();//threadStart事件有訊號 pThread=AfxBeginThread(ThreadFunction,hWnd);//啟動執行緒 pThread->m_bAutoDelete=FALSE; Cview::OnInitialUpdate(); } |
1 2 3 4 5 6 7 |
Void CtestView::OnDestroy() { threadEnd.SetEvent(); WaitForSingleObject(pThread->m_hThread,INFINITE); delete pThread; Cview::OnDestroy(); } |
執行這個程式,當關閉程式時,才顯示提示框,顯示”Thread ended”。
4、執行緒之間的同步
前面我們講過,各個執行緒可以訪問程式中的公共變數,所以使用多執行緒的過程中需要注意的問題是如何防止兩個或兩個以上的執行緒同時訪問同一個資料,以免破壞資料的完整性。保證各個執行緒可以在一起適當的協調工作稱為執行緒之間的同步。前面一節介紹的事件物件實際上就是一種同步形式。Visual C++中使用同步類來解決作業系統的並行性而引起的資料不安全的問題,MFC支援的七個多執行緒的同步類可以分成兩大類:同步物件(CsyncObject、Csemaphore、Cmutex、CcriticalSection和Cevent)和同步訪問物件(CmultiLock和CsingleLock)。本節主要介紹臨界區(critical section)、互斥(mutexe)、訊號量(semaphore),這些同步物件使各個執行緒協調工作,程式執行起來更安全。
(一) 臨界區
臨界區是保證在某一個時間只有一個執行緒可以訪問資料的方法。使用它的過程中,需要給各個執行緒提供一個共享的臨界區物件,無論哪個執行緒佔有臨界區物件,都可以訪問受到保護的資料,這時候其它的執行緒需要等待,直到該執行緒釋放臨界區物件為止,臨界區被釋放後,另外的執行緒可以強佔這個臨界區,以便訪問共享的資料。臨界區對應著一個CcriticalSection物件,當執行緒需要訪問保護資料時,呼叫臨界區物件的Lock()成員函式;當對保護資料的操作完成之後,呼叫臨界區物件的Unlock()成員函式釋放對臨界區物件的擁有權,以使另一個執行緒可以奪取臨界區物件並訪問受保護的資料。同時啟動兩個執行緒,它們對應的函式分別為WriteThread()和ReadThread(),用以對公共陣列組array[]操作,下面的程式碼說明了如何使用臨界區物件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#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,而不是雜亂無章的數,如果不使用同步,則不是這個結果,有興趣的讀者可以實驗一下。
(二)互斥
互斥與臨界區很相似,但是使用時相對複雜一些,它不僅可以在同一應用程式的執行緒間實現同步,還可以在不同的程式間實現同步,從而實現資源的安全共享。互斥與Cmutex類的物件相對應,使用互斥物件時,必須建立一個CSingleLock或CMultiLock物件,用於實際的訪問控制,因為這裡的例子只處理單個互斥,所以我們可以使用CSingleLock物件,該物件的Lock()函式用於佔有互斥,Unlock()用於釋放互斥。實現程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#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(); } |
(三)訊號量
訊號量的用法和互斥的用法很相似,不同的是它可以同一時刻允許多個執行緒訪問同一個資源,建立一個訊號量需要用Csemaphore類宣告一個物件,一旦建立了一個訊號量物件,就可以用它來對資源的訪問技術。要實現計數處理,先建立一個CsingleLock或CmltiLock物件,然後用該物件的Lock()函式減少這個訊號量的計數值,Unlock()反之。下面的程式碼分別啟動三個執行緒,執行時同時顯示二個訊息框,然後10秒後第三個訊息框才得以顯示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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; } |
二、 程式設計步驟
- 啟動Visual C++6.0,生成一個32位的控制檯程式,將該程式命名為”sequence”
- 輸入要排續的數字,宣告四個子執行緒;
- 輸入程式碼,編譯執行程式。
三、 程式程式碼
1 |
sequence.cpp : Defines the entry point for the console application. |
主要用到的WINAPI執行緒控制函式,有關詳細說明請檢視MSDN;
執行緒建立函式:
1 2 3 4 5 6 7 8 |
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全屬性結構指標,可為NULL; DWORD dwStackSize, // 執行緒棧大小,若為0表示使用預設值; LPTHREAD_START_ROUTINE lpStartAddress, // 指向執行緒函式的指標; LPVOID lpParameter, // 傳遞給執行緒函式的引數,可以儲存一個指標值; DWORD dwCreationFlags, // 執行緒建立是的初始標記,執行或掛起; LPDWORD lpThreadId // 指向接收執行緒號的DWORD變數; ); |
對臨界資源控制的多執行緒控制的訊號函式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全屬性結構指標,可為NULL; BOOL bManualReset, // 手動清除訊號標記,TRUE在WaitForSingleObject後必須手動//呼叫RetEvent清除訊號。若為 FALSE則在WaitForSingleObject //後,系統自動清除事件訊號; BOOL bInitialState, // 初始狀態,TRUE有訊號,FALSE無訊號; LPCTSTR lpName // 訊號量的名稱,字元數不可多於MAX_PATH; //如果遇到同名的其他訊號量函式就會失敗,如果遇 //到同類訊號同名也要注意變化; ); HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全屬性結構指標,可為NULL BOOL bInitialOwner, // 當前建立互斥量是否佔有該互斥量TRUE表示佔有, //這樣其他執行緒就不能獲得此互斥量也就無法進入由 //該互斥量控制的臨界區。FALSE表示不佔有該互斥量 LPCTSTR lpName // 訊號量的名稱,字元數不可多於MAX_PATH如果 //遇到同名的其他訊號量函式就會失敗, //如果遇到同類訊號同名也要注意變化; ); //初始化臨界區訊號,使用前必須先初始化 VOID InitializeCriticalSection( LPCRITICAL_SECTION lpCriticalSection // 臨界區變數指標 ); //阻塞函式 //如果等待的訊號量不可用,那麼執行緒就會掛起,直到訊號可用 //執行緒才會被喚醒,該函式會自動修改訊號,如Event,執行緒被喚醒之後 //Event訊號會變得無訊號,Mutex、Semaphore等也會變。 DWORD WaitForSingleObject( HANDLE hHandle, // 等待物件的控制程式碼 DWORD dwMilliseconds // 等待毫秒數,INFINITE表示無限等待 ); //如果要等待多個訊號可以使用WaitForMutipleObject函式 */ |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include "stdafx.h" #include "stdlib.h" #include "memory.h" HANDLE evtTerminate; //事件訊號,標記是否所有子執行緒都執行完 /* 下面使用了三種控制方法,你可以註釋其中兩種,使用其中一種。 注意修改時要連帶修改臨界區PrintResult裡的相應控制語句 */ HANDLE evtPrint; //事件訊號,標記事件是否已發生 //CRITICAL_SECTION csPrint; //臨界區 //HANDLE mtxPrint; //互斥訊號,如有訊號表明已經有執行緒進入臨界區並擁有此訊號 static long ThreadCompleted = 0; /*用來標記四個子執行緒中已完成執行緒的個數,當一個子執行緒完成時就對ThreadCompleted進行加一操作, 要使用InterlockedIncrement(long* lpAddend)和InterlockedDecrement(long* lpAddend)進行加減操作*/ |
1 2 3 4 5 6 |
//下面的結構是用於傳送排序的資料給各個排序子執行緒 struct MySafeArray { long* data; int iLength; }; |
1 2 |
//列印每一個執行緒的排序結果 void PrintResult(long* Array, int iLength, const char* HeadStr = "sort"); |
1 2 3 4 5 |
//排序函式 unsigned long __stdcall BubbleSort(void* theArray); //氣泡排序 unsigned long __stdcall SelectSort(void* theArray); //選擇排序 unsigned long __stdcall HeapSort(void* theArray); //堆排序 unsigned long __stdcall InsertSort(void* theArray); //插入排序 |
以上四個函式的宣告必須適合作為一個執行緒函式的必要條件才可以使用CreateThread建立一個執行緒。
- 呼叫方法必須是__stdcall,即函式引數壓棧順序由右到左,而且由函式本身負責
棧的恢復, C和C++預設是__cdecl, 所以要顯式宣告是__stdcall - 返回值必須是unsigned long
- 引數必須是一個32位值,如一個指標值或long型別
- 如果函式是類成員函式,必須宣告為static函式,在CreateThread時函式指標有特殊的寫法。如下(函式是類CThreadTest的成員函式中):
12static unsigned long _stdcall MyThreadFun(void* pParam);handleRet = CreateThread(NULL, 0, &CThreadTestDlg::MyThreadFun, NULL, 0, &ThreadID);
之所以要宣告為static是由於,該函式必須要獨立於物件例項來使用,即使沒有宣告例項也可以使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
int QuickSort(long* Array, int iLow, int iHigh); //快速排序 int main(int argc, char* argv[]) { long data[] = {123,34,546,754,34,74,3,56}; int iDataLen = 8; //為了對各個子執行緒分別對原始資料進行排序和儲存排序結果 //分別分配記憶體對data陣列的資料進行復制 long *data1, *data2, *data3, *data4, *data5; MySafeArray StructData1, StructData2, StructData3, StructData4; data1 = new long[iDataLen]; memcpy(data1, data, iDataLen << 2); //把data中的資料複製到data1中 //記憶體複製 memcpy(目標記憶體指標, 源記憶體指標, 複製位元組數), 因為long的長度 //為4位元組,所以複製的位元組數為iDataLen << 2, 即等於iDataLen*4 StructData1.data = data1; StructData1.iLength = iDataLen; data2 = new long[iDataLen]; memcpy(data2, data, iDataLen << 2); StructData2.data = data2; StructData2.iLength = iDataLen; data3 = new long[iDataLen]; memcpy(data3, data, iDataLen << 2); StructData3.data = data3; StructData3.iLength = iDataLen; data4 = new long[iDataLen]; memcpy(data4, data, iDataLen << 2); StructData4.data = data4; StructData4.iLength = iDataLen; data5 = new long[iDataLen]; memcpy(data5, data, iDataLen << 2); unsigned long TID1, TID2, TID3, TID4; //對訊號量進行初始化 evtTerminate = CreateEvent(NULL, FALSE, FALSE, "Terminate"); evtPrint = CreateEvent(NULL, FALSE, TRUE, "PrintResult"); //分別建立各個子執行緒 CreateThread(NULL, 0, &BubbleSort, &StructData1, NULL, &TID1); CreateThread(NULL, 0, &SelectSort, &StructData2, NULL, &TID2); CreateThread(NULL, 0, &HeapSort, &StructData3, NULL, &TID3); CreateThread(NULL, 0, &InsertSort, &StructData4, NULL, &TID4); //在主執行緒中執行行快速排序,其他排序在子執行緒中執行 QuickSort(data5, 0, iDataLen - 1); PrintResult(data5, iDataLen, "Quick Sort"); WaitForSingleObject(evtTerminate, INFINITE); //等待所有的子執行緒結束 //所有的子執行緒結束後,主執行緒才可以結束 delete[] data1; delete[] data2; delete[] data3; delete[] data4; CloseHandle(evtPrint); return 0; } |
氣泡排序思想(升序,降序同理,後面的演算法一樣都是升序):從頭到尾對資料進行兩兩比較進行交換,小的放前大的放後。這樣一次下來,最大的元素就會被交換的最後,然後下一次
迴圈就不用對最後一個元素進行比較交換了,所以呢每一次比較交換的次數都比上一次迴圈的次數少一,這樣N次之後資料就變得升序排列了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
unsigned long __stdcall BubbleSort(void* theArray) { long* Array = ((MySafeArray*)theArray)->data; int iLength = ((MySafeArray*)theArray)->iLength; int i, j=0; long swap; for (i = iLength-1; i >0; i--) { for(j = 0; j < i; j++) { if(Array[j] >Array[j+1]) //前比後大,交換 { swap = Array[j]; Array[j] = Array[j+1]; Array[j+1] = swap; } } } PrintResult(Array, iLength, "Bubble Sort"); //向控制檯列印排序結果 InterlockedIncrement(&ThreadCompleted); //返回前使執行緒完成數標記加1 if(ThreadCompleted == 4) SetEvent(evtTerminate); //檢查是否其他執行緒都已執行完 //若都執行完則設定程式結束訊號量 return 0; } |
選擇排序思想:每一次都從無序的資料中找出最小的元素,然後和前面已經有序的元素序列的後一個元素進行交換,這樣整個源序列就會分成兩部分,前面一部分是已經排好序的有序序列,後面一部分是無序的,用於選出最小的元素。迴圈N次之後,前面的有序序列加長到跟源序列一樣長,後面的無序部分長度變為0,排序就完成了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
unsigned long __stdcall SelectSort(void* theArray) { long* Array = ((MySafeArray*)theArray)->data; int iLength = ((MySafeArray*)theArray)->iLength; long lMin, lSwap; int i, j, iMinPos; for(i=0; i < iLength-1; i++) { lMin = Array[i]; iMinPos = i; for(j=i + 1; j <= iLength-1; j++) //從無序的元素中找出最小的元素 { if(Array[j] < lMin) { iMinPos = j; lMin = Array[j]; } } //把選出的元素交換拼接到有序序列的最後 lSwap = Array[i]; Array[i] = Array[iMinPos]; Array[iMinPos] = lSwap; } PrintResult(Array, iLength, "Select Sort"); //向控制檯列印排序結果 InterlockedIncrement(&ThreadCompleted); //返回前使執行緒完成數標記加1 if(ThreadCompleted == 4) SetEvent(evtTerminate);//檢查是否其他執行緒都已執行完 //若都執行完則設定程式結束訊號量 return 0; } |
堆排序思想:堆:資料元素從1到N排列成一棵二叉樹,而且這棵樹的每一個子樹的根都是該樹中的元素的最小或最大的元素這樣如果一個無序資料集合是一個堆那麼,根元素就是最小或最大的元素堆排序就是不斷對剩下的資料建堆,把最小或最大的元素析透出來。下面的演算法,就是從最後一個元素開始,依據一個節點比父節點數值大的原則對所有元素進行調整,這樣調整一次就形成一個堆,第一個元素就是最小的元素。然後再對剩下的無序資料再進行建堆,注意這時後面的無序資料元素的序數都要改變,如第一次建堆後,第二個元素就會變成堆的第一個元素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
unsigned long __stdcall HeapSort(void* theArray) { long* Array = ((MySafeArray*)theArray)->data; int iLength = ((MySafeArray*)theArray)->iLength; int i, j, p; long swap; for(i=0; i { for(j = iLength - 1; j>i; j--) //從最後倒數上去比較位元組點和父節點 { p = (j - i - 1)/2 + i; //計算父節點陣列下標 //注意到樹節點序數跟陣列下標不是等同的,因為建堆的元素個數逐個遞減 if(Array[j] < Array[p]) //如果父節點數值大則交換父節點和位元組點 { swap = Array[j]; Array[j] = Array[p]; Array[p] = swap; } } } PrintResult(Array, iLength, "Heap Sort"); //向控制檯列印排序結果 InterlockedIncrement(&ThreadCompleted); //返回前使執行緒完成數標記加1 if(ThreadCompleted == 4) SetEvent(evtTerminate); //檢查是否其他執行緒都已執行完 //若都執行完則設定程式結束訊號量 return 0; } |
插入排序思想:把源資料序列看成兩半,前面一半是有序的,後面一半是無序的,把無序的資料從頭到尾逐個逐個的插入到前面的有序資料中,使得有序的資料的個數不斷增大,同時無序的資料個數就越來越少,最後所有元素都會變得有序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
unsigned long __stdcall InsertSort(void* theArray) { long* Array = ((MySafeArray*)theArray)->data; int iLength = ((MySafeArray*)theArray)->iLength; int i=1, j=0; long temp; for(i=1; i { temp = Array[i]; //取出序列後面無序資料的第一個元素值 for(j=i; j>0; j--) //和前面的有序資料逐個進行比較找出合適的插入位置 { if(Array[j - 1] >temp) //如果該元素比插入值大則後移 Array[j] = Array[j - 1]; else //如果該元素比插入值小,那麼該位置的後一位就是插入元素的位置 break; } Array[j] = temp; } PrintResult(Array, iLength, "Insert Sort"); //向控制檯列印排序結果 InterlockedIncrement(&ThreadCompleted); //返回前使執行緒完成數標記加1 if(ThreadCompleted == 4) SetEvent(evtTerminate); //檢查是否其他執行緒都已執行完 //若都執行完則設定程式結束訊號量 return 0; } |
快速排序思想:快速排序是分治思想的一種應用,它先選取一個支點,然後把小於支點的元素交換到支點的前邊,把大於支點的元素交換到支點的右邊。然後再對支點左邊部分和右邊部分進行同樣的處理,這樣若干次之後,資料就會變得有序。下面的實現使用了遞迴建立兩個遊標:iLow,iHigh;iLow指向序列的第一個元素,iHigh指向最後一個先選第一個元素作為支點,並把它的值存貯在一個輔助變數裡。那麼第一個位置就變為空並可以放置其他的元素。 這樣從iHigh指向的元素開始向前移動遊標,iHigh查詢比支點小的元素,如果找到,則把它放置到空置了的位置(現在是第一個位置),然後iHigh遊標停止移動,這時iHigh指向的位置被空置,然後移動iLow遊標尋找比支點大的元素放置到iHigh指向的空置的位置,如此往復直到iLow與iHigh相等。最後使用遞迴對左右兩部分進行同樣處理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
int QuickSort(long* Array, int iLow, int iHigh) { if(iLow >= iHigh) return 1; //遞迴結束條件 long pivot = Array[iLow]; int iLowSaved = iLow, iHighSaved = iHigh; //保未改變的iLow,iHigh值儲存起來 while (iLow < iHigh) { while (Array[iHigh] >= pivot && iHigh >iLow) //尋找比支點大的元素 iHigh -- ; Array[iLow] = Array[iHigh]; //把找到的元素放置到空置的位置 while (Array[iLow] < pivot && iLow < iHigh) //尋找比支點小的元素 iLow ++ ; Array[iHigh] = Array[iLow]; //把找到的元素放置到空置的位置 } Array[iLow] = pivot; //把支點值放置到支點位置,這時支點位置是空置的 //對左右部分分別進行遞迴處理 QuickSort(Array, iLowSaved, iHigh-1); QuickSort(Array, iLow+1, iHighSaved); return 0; } //每一個執行緒都要使用這個函式進行輸出,而且只有一個顯示器,產生多個執行緒 //競爭對控制檯的使用權。 void PrintResult(long* Array, int iLength, const char* HeadStr) { WaitForSingleObject(evtPrint, INFINITE); //等待事件有訊號 //EnterCriticalSection(&csPrint); //標記有執行緒進入臨界區 //WaitForSingleObject(mtxPrint, INFINITE); //等待互斥量空置(沒有執行緒擁有它) int i; printf("%s: ", HeadStr); for (i=0; i { printf("%d,", Array[i]); Sleep(100); //延時(可以去掉) /*只是使得多執行緒對臨界區訪問的問題比較容易看得到 如果你把臨界控制的語句註釋掉,輸出就會變得很凌亂,各個排序的結果會 分插間隔著輸出,如果不延時就不容易看到這種不對臨界區控制的結果 */ } printf("%d\n", Array[i]); SetEvent(evtPrint); //把事件訊號量恢復,變為有訊號 } |
四、 小結
對複雜的應用程式來說,執行緒的應用給應用程式提供了高效、快速、安全的資料處理能力。本例項講述了執行緒處理中經常遇到的問題,希望對讀者朋友有一定的幫助,起到拋磚引玉的作用。