從單進程單線程到多進程多線程是操作系統發展的一種必然趨勢,當年的DOS系統屬于單任務操作系統,最優秀的程序員也只能通過駐留內存的方式實現所謂的"多任務",而如今的Win32操作系統卻可以一邊聽音樂,一邊編程,一邊打印文檔。
從單進程單線程到多進程多線程是操作系統發展的一種必然趨勢,當年的DOS系統屬于單任務操作系統,最優秀的程序員也只能通過駐留內存的方式實現所謂的"多任務",而如今的Win32操作系統卻可以一邊聽音樂,一邊編程,一邊打印文檔。
理解多線程及其同步、互斥等通信方式是理解現代操作系統的關鍵一環,當我們精通了Win32多線程程序設計后,理解和學習其它操作系統的多任務控制也非常容易。許多程序員從來沒有學習過嵌入式系統領域著名的操作系統VxWorks,但是立馬就能在上面做開發,大概要歸功于平時在Win32多線程上下的功夫。
因此,學習Win32多線程不僅對理解Win32本身有重要意義,而且對學習和領會其它操作系統也有觸類旁通的作用。
進程與線程
先闡述一下進程和線程的概念和區別,這是一個許多大學老師也講不清楚的問題。
進程(Process)是具有一定獨立功能的程序關于某個數據集合上的一次運行活動,是系統進行資源分配和調度的一個獨立單位。程序只是一組指令的有序集合,它本身沒有任何運行的含義,只是一個靜態實體。而進程則不同,它是程序在某個數據集上的執行,是一個動態實體。它因創建而產生,因調度而運行,因等待資源或事件而被處于等待狀態,因完成任務而被撤消,反映了一個程序在一定的數據集上運行的全部動態過程。
線程(Thread)是進程的一個實體,是CPU調度和分派的基本單位。線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
線程和進程的關系是:線程是屬于進程的,線程運行在進程空間內,同一進程所產生的線程共享同一內存空間,當進程退出時該進程所產生的線程都會被強制退出并清除。線程可與屬于同一進程的其它線程共享進程所擁有的全部資源,但是其本身基本上不擁有系統資源,只擁有一點在運行中必不可少的信息(如程序計數器、一組寄存器和棧)。
根據進程與線程的設置,操作系統大致分為如下類型:
(1)單進程、單線程,MS-DOS大致是這種操作系統;
(2)多進程、單線程,多數UNIX(及類UNIX的LINUX)是這種操作系統;
(3)多進程、多線程,Win32(Windows NT/2000/XP等)、Solaris 2.x和OS/2都是這種操作系統;
(4)單進程、多線程,VxWorks是這種操作系統。
在操作系統中引入線程帶來的主要好處是:
(1)在進程內創建、終止線程比創建、終止進程要快;
(2)同一進程內的線程間切換比進程間的切換要快,尤其是用戶級線程間的切換。另外,線程的出現還因為以下幾個原因:
(1)并發程序的并發執行,在多處理環境下更為有效。一個并發程序可以建立一個進程,而這個并發程序中的若干并發程序段就可以分別建立若干線程,使這些線程在不同的處理機上執行。
(2)每個進程具有獨立的地址空間,而該進程內的所有線程共享該地址空間。這樣可以解決父子進程模型中,子進程必須復制父進程地址空間的問題。
(3)線程對解決客戶/服務器模型非常有效。
Win32進程
1、進程間通信(IPC)
Win32進程間通信的方式主要有:
(1)剪貼板(Clip Board);
(2)動態數據交換(Dynamic Data Exchange);
(3)部件對象模型(Component Object Model);
(4)文件映射(File Mapping);
(5)郵件槽(Mail Slots);
(6)管道(Pipes);
(7)Win32套接字(Socket);
(8)遠程過程調用(Remote Procedure Call);
(9)WM_COPYDATA消息(WM_COPYDATA Message)。
2、獲取進程信息
在WIN32中,可使用在PSAPI .DLL中提供的Process status Helper函數幫助我們獲取進程信息。
(1)EnumProcesses()函數可以獲取進程的ID,其原型為:
參數lpidProcess:一個足夠大的DWORD類型的數組,用于存放進程的ID值;
參數cb:存放進程ID值的數組的最大長度,是一個DWORD類型的數據;
參數cbNeeded:指向一個DWORD類型數據的指針,用于返回進程的數目;
函數返回值:如果調用成功,返回TRUE,同時將所有進程的ID值存放在lpidProcess參數所指向的數組中,進程個數存放在cbNeeded參數所指向的變量中;如果調用失敗,返回FALSE。
(2)GetModuleFileNameExA()函數可以實現通過進程句柄獲取進程文件名,其原型為:
參數hProcess:接受進程句柄的參數,是HANDLE類型的變量;
參數hModule:指針型參數,在本文的程序中取值為NULL;
參數lpstrFileName:LPTSTR類型的指針,用于接受主調函數傳遞來的用于存放進程名的字符數組指針;
參數nsize:lpstrFileName所指數組的長度;
函數返回值:如果調用成功,返回一個大于0的DWORD類型的數據,同時將hProcess所對應的進程名存放在lpstrFileName參數所指向的數組中;加果調用失敗,則返回0。
通過下列代碼就可以遍歷系統中的進程,獲得進程列表:
//獲取當前進程總數
EnumProcesses(process_ids, sizeof(process_ids), &num_processes);
//遍歷進程
for (int i = 0; i < num_processes; i++)
{
//根據進程ID獲取句柄
process[i] = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, 0,
process_ids[i]);
//通過句柄獲取進程文件名
if (GetModuleFileNameExA(process[i], NULL, File_name, sizeof(fileName)))
cout << fileName << endl;
}
1.線程函數
在啟動一個線程之前,必須為線程編寫一個全局的線程函數,這個線程函數接受一個32位的LPVOID作為參數,返回一個UINT,線程函數的結構為:
在線程處理代碼部分通常包括一個死循環,該循環中先等待某事情的發生,再處理相關的工作:
一般來說,C++的類成員函數不能作為線程函數。這是因為在類中定義的成員函數,編譯器會給其加上this指針。請看下列程序:
程序編譯時出現如下錯誤:
再看下列程序:
程序編譯時會出錯:
如果一定要以類成員函數作為線程函數,通常有如下解決方案:
(1)將該成員函數聲明為static類型,去掉this指針;
我們將上述二個程序改變為:
均編譯通過。
將成員函數聲明為靜態雖然可以解決作為線程函數的問題,但是它帶來了新的問題,那就是static成員函數只能訪問static成員。解決此問題的一種途徑是可以在調用類靜態成員函數(線程函數)時將this指針作為參數傳入,并在改線程函數中用強制類型轉換將this轉換成指向該類的指針,通過該指針訪問非靜態成員。
(2)不定義類成員函數為線程函數,而將線程函數定義為類的友元函數。這樣,線程函數也可以有類成員函數同等的權限;
我們將程序修改為:
(3)可以對非靜態成員函數實現回調,并訪問非靜態成員,此法涉及到一些高級技巧,在此不再詳述。
線程之間通信的兩個基本問題是互斥和同步。
線程同步是指線程之間所具有的一種制約關系,一個線程的執行依賴另一個線程的消息,當它沒有得到另一個線程的消息時應等待,直到消息到達時才被喚醒。
線程互斥是指對于共享的操作系統資源(指的是廣義的"資源",而不是Windows的.res文件,譬如全局變量就是一種共享資源),在各線程訪問時的排它性。當有若干個線程都要使用某一共享資源時,任何時刻最多只允許一個線程去使用,其它要使用該資源的線程必須等待,直到占用資源者釋放該資源。
線程互斥是一種特殊的線程同步。
實際上,互斥和同步對應著線程間通信發生的兩種情況:
(1)當有多個線程訪問共享資源而不使資源被破壞時;
(2)當一個線程需要將某個任務已經完成的情況通知另外一個或多個線程時。
在WIN32中,同步機制主要有以下幾種:
(1)事件(Event);
(2)信號量(semaphore);
(3)互斥量(mutex);
(4)臨界區(Critical section)。
全局變量
因為進程中的所有線程均可以訪問所有的全局變量,因而全局變量成為Win32多線程通信的最簡單方式。例如:
上述程序中使用全局變量和while循環查詢進行線程間同步,實際上,這是一種應該避免的方法,因為:
(1)當主線程必須使自己與ThreadFunc函數的完成運行實現同步時,它并沒有使自己進入睡眠狀態。由于主線程沒有進入睡眠狀態,因此操作系統繼續為它調度C P U時間,這就要占用其他線程的寶貴時間周期;
(2)當主線程的優先級高于執行ThreadFunc函數的線程時,就會發生globalFlag永遠不能被賦值為true的情況。因為在這種情況下,系統決不會將任何時間片分配給ThreadFunc線程。
事件
事件(Event)是WIN32提供的最靈活的線程間同步方式,事件可以處于激發狀態(signaled or true)或未激發狀態(unsignal or false)。根據狀態變遷方式的不同,事件可分為兩類:
(1)手動設置:這種對象只可能用程序手動設置,在需要該事件或者事件發生時,采用SetEvent及ResetEvent來進行設置。
(2)自動恢復:一旦事件發生并被處理后,自動恢復到沒有事件狀態,不需要再次設置。
創建事件的函數原型為:
使用"事件"機制應注意以下事項:
(1)如果跨進程訪問事件,必須對事件命名,在對事件命名的時候,要注意不要與系統命名空間中的其它全局命名對象沖突;
(2)事件是否要自動恢復;
(3)事件的初始狀態設置。
由于event對象屬于內核對象,故進程B可以調用OpenEvent函數通過對象的名字獲得進程A中event對象的句柄,然后將這個句柄用于ResetEvent、SetEvent和WaitForMultipleObjects等函數中。此法可以實現一個進程的線程控制另一進程中線程的運行,例如:
HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent");
ResetEvent(hEvent);
在MFC程序中創建一個線程,宜調用AfxBeginThread函數。該函數因參數不同而具有兩種重載版本,分別對應工作者線程和用戶接口(UI)線程。
工作者線程
工作者線程編程較為簡單,只需編寫線程控制函數和啟動線程即可。下面的代碼給出了定義一個控制函數和啟動它的過程:
UI線程
創建用戶界面線程時,必須首先從CWinThread 派生類,并使用 DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE 宏聲明此類。
下面給出了CWinThread類的原型(添加了關于其重要函數功能和是否需要被繼承類重載的注釋):
啟動UI線程的AfxBeginThread函數的原型為:
我們可以方便地使用VC++ 6.0類向導定義一個繼承自CWinThread的用戶線程類。下面給出產生我們自定義的CWinThread子類CMyUIThread的方法。
打開VC++ 6.0類向導,在如下窗口中選擇Base Class類為CWinThread,輸入子類名為CMyUIThread,點擊"OK"按鈕后就產生了類CMyUIThread。
其源代碼框架為:
使用下列代碼就可以啟動這個UI線程:
另外,我們也可以不用AfxBeginThread 創建線程,而是分如下兩步完成:
(1)調用線程類的構造函數創建一個線程對象;
(2)調用CWinThread::CreateThread函數來啟動該線程。
在線程自身內調用AfxEndThread函數可以終止該線程:
對于UI線程而言,如果消息隊列中放入了WM_QUIT消息,將結束線程。
關于UI線程和工作者線程的分配,最好的做法是:將所有與UI相關的操作放入主線程,其它的純粹的運算工作交給獨立的數個工作者線程。
候捷先生早些時間喜歡為MDI程序的每個窗口創建一個線程,他后來澄清了這個錯誤。因為如果為MDI程序的每個窗口都單獨創建一個線程,在窗口進行切換的時候,將進行線程的上下文切換!
而網絡通信也是多線程應用最廣泛的領域之一,所以本章的最后一節也將對多線程網絡通信進行簡短的描述。
1.串口通信
在工業控制系統中,工控機(一般都基于PC Windows平臺)經常需要與單片機通過串口進行通信。因此,操作和使用PC的串口成為大多數單片機、嵌入式系統領域工程師必須具備的能力。
串口的使用需要通過三個步驟來完成的:
(1) 打開通信端口;
(2) 初始化串口,設置波特率、數據位、停止位、奇偶校驗等參數。為了給讀者一個直觀的印象,下圖從Windows的"控制面板->系統->設備管理器->通信端口(COM1)"打開COM的設置窗口:
(3) 讀寫串口。
在WIN32平臺下,對通信端口進行操作跟基本的文件操作一樣。
創建/打開COM資源
下列函數如果調用成功,則返回一個標識通信端口的句柄,否則返回-1:
獲得/設置COM屬性
下列函數可以獲得COM口的設備控制塊,從而獲得相關參數:
如果要調整通信端口的參數,則需要重新配置設備控制塊,再用WIN32 API SetCommState()函數進行設置:
DCB結構包含了串口的各項參數設置,如下:
讀寫串口
在讀寫串口之前,還要用PurgeComm()函數清空緩沖區,并用SetCommMask ()函數設置事件掩模來監視指定通信端口上的事件,其原型為:
串口上可能發生的事件如下表所示:
在設置好事件掩模后,我們就可以利用WaitCommEvent()函數來等待串口上發生事件,其函數原型為:
我們可以在發生事件后,根據相應的事件類型,進行串口的讀寫操作:
BOOL ReadFile(HANDLE hFile, //標識通信端口的句柄
LPVOID lpBuffer, //輸入數據Buffer指針
DWORD nNumberOfBytesToRead, // 需要讀取的字節數
LPDWORD lpNumberOfBytesRead, //實際讀取的字節數指針
LPOVERLAPPED lpOverlapped //指向overlapped結構
);
BOOL WriteFile(HANDLE hFile, //標識通信端口的句柄
LPCVOID lpBuffer, //輸出數據Buffer指針
DWORD nNumberOfBytesToWrite, //需要寫的字節數
LPDWORD lpNumberOfBytesWritten, //實際寫入的字節數指針
LPOVERLAPPED lpOverlapped //指向overlapped結構
);
理解多線程及其同步、互斥等通信方式是理解現代操作系統的關鍵一環,當我們精通了Win32多線程程序設計后,理解和學習其它操作系統的多任務控制也非常容易。因此,學習Win32多線程不僅對理解Win32本身有重要意義,而且對學習和領會其它操作系統也有觸類旁通的作用。
深入淺出Win32多線程程序設計之基本概念
引言從單進程單線程到多進程多線程是操作系統發展的一種必然趨勢,當年的DOS系統屬于單任務操作系統,最優秀的程序員也只能通過駐留內存的方式實現所謂的"多任務",而如今的Win32操作系統卻可以一邊聽音樂,一邊編程,一邊打印文檔。
理解多線程及其同步、互斥等通信方式是理解現代操作系統的關鍵一環,當我們精通了Win32多線程程序設計后,理解和學習其它操作系統的多任務控制也非常容易。許多程序員從來沒有學習過嵌入式系統領域著名的操作系統VxWorks,但是立馬就能在上面做開發,大概要歸功于平時在Win32多線程上下的功夫。
因此,學習Win32多線程不僅對理解Win32本身有重要意義,而且對學習和領會其它操作系統也有觸類旁通的作用。
進程與線程
先闡述一下進程和線程的概念和區別,這是一個許多大學老師也講不清楚的問題。
進程(Process)是具有一定獨立功能的程序關于某個數據集合上的一次運行活動,是系統進行資源分配和調度的一個獨立單位。程序只是一組指令的有序集合,它本身沒有任何運行的含義,只是一個靜態實體。而進程則不同,它是程序在某個數據集上的執行,是一個動態實體。它因創建而產生,因調度而運行,因等待資源或事件而被處于等待狀態,因完成任務而被撤消,反映了一個程序在一定的數據集上運行的全部動態過程。
線程(Thread)是進程的一個實體,是CPU調度和分派的基本單位。線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
線程和進程的關系是:線程是屬于進程的,線程運行在進程空間內,同一進程所產生的線程共享同一內存空間,當進程退出時該進程所產生的線程都會被強制退出并清除。線程可與屬于同一進程的其它線程共享進程所擁有的全部資源,但是其本身基本上不擁有系統資源,只擁有一點在運行中必不可少的信息(如程序計數器、一組寄存器和棧)。
根據進程與線程的設置,操作系統大致分為如下類型:
(1)單進程、單線程,MS-DOS大致是這種操作系統;
(2)多進程、單線程,多數UNIX(及類UNIX的LINUX)是這種操作系統;
(3)多進程、多線程,Win32(Windows NT/2000/XP等)、Solaris 2.x和OS/2都是這種操作系統;
(4)單進程、多線程,VxWorks是這種操作系統。
在操作系統中引入線程帶來的主要好處是:
(1)在進程內創建、終止線程比創建、終止進程要快;
(2)同一進程內的線程間切換比進程間的切換要快,尤其是用戶級線程間的切換。另外,線程的出現還因為以下幾個原因:
(1)并發程序的并發執行,在多處理環境下更為有效。一個并發程序可以建立一個進程,而這個并發程序中的若干并發程序段就可以分別建立若干線程,使這些線程在不同的處理機上執行。
(2)每個進程具有獨立的地址空間,而該進程內的所有線程共享該地址空間。這樣可以解決父子進程模型中,子進程必須復制父進程地址空間的問題。
(3)線程對解決客戶/服務器模型非常有效。
Win32進程
1、進程間通信(IPC)
Win32進程間通信的方式主要有:
(1)剪貼板(Clip Board);
(2)動態數據交換(Dynamic Data Exchange);
(3)部件對象模型(Component Object Model);
(4)文件映射(File Mapping);
(5)郵件槽(Mail Slots);
(6)管道(Pipes);
(7)Win32套接字(Socket);
(8)遠程過程調用(Remote Procedure Call);
(9)WM_COPYDATA消息(WM_COPYDATA Message)。
2、獲取進程信息
在WIN32中,可使用在PSAPI .DLL中提供的Process status Helper函數幫助我們獲取進程信息。
(1)EnumProcesses()函數可以獲取進程的ID,其原型為:
BOOL EnumProcesses(DWORD * lpidProcess, DWORD cb, DWORD*cbNeeded); |
參數lpidProcess:一個足夠大的DWORD類型的數組,用于存放進程的ID值;
參數cb:存放進程ID值的數組的最大長度,是一個DWORD類型的數據;
參數cbNeeded:指向一個DWORD類型數據的指針,用于返回進程的數目;
函數返回值:如果調用成功,返回TRUE,同時將所有進程的ID值存放在lpidProcess參數所指向的數組中,進程個數存放在cbNeeded參數所指向的變量中;如果調用失敗,返回FALSE。
(2)GetModuleFileNameExA()函數可以實現通過進程句柄獲取進程文件名,其原型為:
DWORD GetModuleFileNameExA(HANDLE hProcess, HMODULE hModule,LPTSTR lpstrFileName, DWORD nsize); |
參數hProcess:接受進程句柄的參數,是HANDLE類型的變量;
參數hModule:指針型參數,在本文的程序中取值為NULL;
參數lpstrFileName:LPTSTR類型的指針,用于接受主調函數傳遞來的用于存放進程名的字符數組指針;
參數nsize:lpstrFileName所指數組的長度;
函數返回值:如果調用成功,返回一個大于0的DWORD類型的數據,同時將hProcess所對應的進程名存放在lpstrFileName參數所指向的數組中;加果調用失敗,則返回0。
通過下列代碼就可以遍歷系統中的進程,獲得進程列表:
//獲取當前進程總數
EnumProcesses(process_ids, sizeof(process_ids), &num_processes);
//遍歷進程
for (int i = 0; i < num_processes; i++)
{
//根據進程ID獲取句柄
process[i] = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, 0,
process_ids[i]);
//通過句柄獲取進程文件名
if (GetModuleFileNameExA(process[i], NULL, File_name, sizeof(fileName)))
cout << fileName << endl;
}
深入淺出Win32多線程程序設計之線程控制
WIN32線程控制主要實現線程的創建、終止、掛起和恢復等操作,這些操作都依賴于WIN32提供的一組API和具體編譯器的C運行時庫函數。1.線程函數
在啟動一個線程之前,必須為線程編寫一個全局的線程函數,這個線程函數接受一個32位的LPVOID作為參數,返回一個UINT,線程函數的結構為:
UINT ThreadFunction(LPVOID pParam) { //線程處理代碼 return0; } |
在線程處理代碼部分通常包括一個死循環,該循環中先等待某事情的發生,再處理相關的工作:
while(1) { WaitForSingleObject(…,…);//或WaitForMultipleObjects(…) //Do something } |
一般來說,C++的類成員函數不能作為線程函數。這是因為在類中定義的成員函數,編譯器會給其加上this指針。請看下列程序:
#include "windows.h" #include <process.h> class ExampleTask { public: void taskmain(LPVOID param); void StartTask(); }; void ExampleTask::taskmain(LPVOID param) {} void ExampleTask::StartTask() { _beginthread(taskmain,0,NULL); } int main(int argc, char* argv[]) { ExampleTask realTimeTask; realTimeTask.StartTask(); return 0; } |
程序編譯時出現如下錯誤:
error C2664: '_beginthread' : cannot convert parameter 1 from 'void (void *)' to 'void (__cdecl *)(void *)' None of the functions with this name in scope match the target type |
再看下列程序:
#include "windows.h" #include <process.h> class ExampleTask { public: void taskmain(LPVOID param); }; void ExampleTask::taskmain(LPVOID param) {} int main(int argc, char* argv[]) { ExampleTask realTimeTask; _beginthread(ExampleTask::taskmain,0,NULL); return 0; } |
程序編譯時會出錯:
error C2664: '_beginthread' : cannot convert parameter 1 from 'void (void *)' to 'void (__cdecl *)(void *)' None of the functions with this name in scope match the target type |
如果一定要以類成員函數作為線程函數,通常有如下解決方案:
(1)將該成員函數聲明為static類型,去掉this指針;
我們將上述二個程序改變為:
#include "windows.h" #include <process.h> class ExampleTask { public: void static taskmain(LPVOID param); void StartTask(); }; void ExampleTask::taskmain(LPVOID param) {} void ExampleTask::StartTask() { _beginthread(taskmain,0,NULL); } int main(int argc, char* argv[]) { ExampleTask realTimeTask; realTimeTask.StartTask(); return 0; } 和 #include "windows.h" #include <process.h> class ExampleTask { public: void static taskmain(LPVOID param); }; void ExampleTask::taskmain(LPVOID param) {} int main(int argc, char* argv[]) { _beginthread(ExampleTask::taskmain,0,NULL); return 0; } |
均編譯通過。
將成員函數聲明為靜態雖然可以解決作為線程函數的問題,但是它帶來了新的問題,那就是static成員函數只能訪問static成員。解決此問題的一種途徑是可以在調用類靜態成員函數(線程函數)時將this指針作為參數傳入,并在改線程函數中用強制類型轉換將this轉換成指向該類的指針,通過該指針訪問非靜態成員。
(2)不定義類成員函數為線程函數,而將線程函數定義為類的友元函數。這樣,線程函數也可以有類成員函數同等的權限;
我們將程序修改為:
#include "windows.h" #include <process.h> class ExampleTask { public: friend void taskmain(LPVOID param); void StartTask(); }; void taskmain(LPVOID param) { ExampleTask * pTaskMain = (ExampleTask *) param; //通過pTaskMain指針引用 } void ExampleTask::StartTask() { _beginthread(taskmain,0,this); } int main(int argc, char* argv[]) { ExampleTask realTimeTask; realTimeTask.StartTask(); return 0; } |
(3)可以對非靜態成員函數實現回調,并訪問非靜態成員,此法涉及到一些高級技巧,在此不再詳述。
深入淺出Win32多線程程序設計之線程通信
簡介線程之間通信的兩個基本問題是互斥和同步。
線程同步是指線程之間所具有的一種制約關系,一個線程的執行依賴另一個線程的消息,當它沒有得到另一個線程的消息時應等待,直到消息到達時才被喚醒。
線程互斥是指對于共享的操作系統資源(指的是廣義的"資源",而不是Windows的.res文件,譬如全局變量就是一種共享資源),在各線程訪問時的排它性。當有若干個線程都要使用某一共享資源時,任何時刻最多只允許一個線程去使用,其它要使用該資源的線程必須等待,直到占用資源者釋放該資源。
線程互斥是一種特殊的線程同步。
實際上,互斥和同步對應著線程間通信發生的兩種情況:
(1)當有多個線程訪問共享資源而不使資源被破壞時;
(2)當一個線程需要將某個任務已經完成的情況通知另外一個或多個線程時。
在WIN32中,同步機制主要有以下幾種:
(1)事件(Event);
(2)信號量(semaphore);
(3)互斥量(mutex);
(4)臨界區(Critical section)。
全局變量
因為進程中的所有線程均可以訪問所有的全局變量,因而全局變量成為Win32多線程通信的最簡單方式。例如:
int var; //全局變量 UINT ThreadFunction(LPVOIDpParam) { var = 0; while (var < MaxValue) { //線程處理 ::InterlockedIncrement(long*) &var); } return 0; } 請看下列程序: int globalFlag = false; DWORD WINAPI ThreadFunc(LPVOID n) { Sleep(2000); globalFlag = true; return 0; } int main() { HANDLE hThrd; DWORD threadId; hThrd = CreateThread(NULL, 0, ThreadFunc, NULL, 0, &threadId); if (hThrd) { printf("Thread launched\n"); CloseHandle(hThrd); } while (!globalFlag) ; printf("exit\n"); } |
上述程序中使用全局變量和while循環查詢進行線程間同步,實際上,這是一種應該避免的方法,因為:
(1)當主線程必須使自己與ThreadFunc函數的完成運行實現同步時,它并沒有使自己進入睡眠狀態。由于主線程沒有進入睡眠狀態,因此操作系統繼續為它調度C P U時間,這就要占用其他線程的寶貴時間周期;
(2)當主線程的優先級高于執行ThreadFunc函數的線程時,就會發生globalFlag永遠不能被賦值為true的情況。因為在這種情況下,系統決不會將任何時間片分配給ThreadFunc線程。
事件
事件(Event)是WIN32提供的最靈活的線程間同步方式,事件可以處于激發狀態(signaled or true)或未激發狀態(unsignal or false)。根據狀態變遷方式的不同,事件可分為兩類:
(1)手動設置:這種對象只可能用程序手動設置,在需要該事件或者事件發生時,采用SetEvent及ResetEvent來進行設置。
(2)自動恢復:一旦事件發生并被處理后,自動恢復到沒有事件狀態,不需要再次設置。
創建事件的函數原型為:
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, // SECURITY_ATTRIBUTES結構指針,可為NULL BOOL bManualReset, // 手動/自動 // TRUE:在WaitForSingleObject后必須手動調用ResetEvent清除信號 // FALSE:在WaitForSingleObject后,系統自動清除事件信號 BOOL bInitialState, //初始狀態 LPCTSTR lpName //事件的名稱 ); |
使用"事件"機制應注意以下事項:
(1)如果跨進程訪問事件,必須對事件命名,在對事件命名的時候,要注意不要與系統命名空間中的其它全局命名對象沖突;
(2)事件是否要自動恢復;
(3)事件的初始狀態設置。
由于event對象屬于內核對象,故進程B可以調用OpenEvent函數通過對象的名字獲得進程A中event對象的句柄,然后將這個句柄用于ResetEvent、SetEvent和WaitForMultipleObjects等函數中。此法可以實現一個進程的線程控制另一進程中線程的運行,例如:
HANDLE hEvent=OpenEvent(EVENT_ALL_ACCESS,true,"MyEvent");
ResetEvent(hEvent);
深入淺出Win32多線程設計之MFC的多線程
1、創建和終止線程在MFC程序中創建一個線程,宜調用AfxBeginThread函數。該函數因參數不同而具有兩種重載版本,分別對應工作者線程和用戶接口(UI)線程。
工作者線程
CWinThread *AfxBeginThread( AFX_THREADPROC pfnThreadProc, //控制函數 LPVOID pParam, //傳遞給控制函數的參數 int nPriority = THREAD_PRIORITY_NORMAL, //線程的優先級 UINT nStackSize = 0, //線程的堆棧大小 DWORD dwCreateFlags = 0, //線程的創建標志 LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL //線程的安全屬性 ); |
工作者線程編程較為簡單,只需編寫線程控制函數和啟動線程即可。下面的代碼給出了定義一個控制函數和啟動它的過程:
//線程控制函數 UINT MfcThreadProc(LPVOID lpParam) { CExampleClass *lpObject = (CExampleClass*)lpParam; if (lpObject == NULL || !lpObject->IsKindof(RUNTIME_CLASS(CExampleClass))) return - 1; //輸入參數非法 //線程成功啟動 while (1) { ...// } return 0; } //在MFC程序中啟動線程 AfxBeginThread(MfcThreadProc, lpObject); |
UI線程
創建用戶界面線程時,必須首先從CWinThread 派生類,并使用 DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE 宏聲明此類。
下面給出了CWinThread類的原型(添加了關于其重要函數功能和是否需要被繼承類重載的注釋):
class CWinThread : public CCmdTarget { DECLARE_DYNAMIC(CWinThread) public: // Constructors CWinThread(); BOOL CreateThread(DWORD dwCreateFlags = 0, UINT nStackSize = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL); // Attributes CWnd* m_pMainWnd; // main window (usually same AfxGetApp()->m_pMainWnd) CWnd* m_pActiveWnd; // active main window (may not be m_pMainWnd) BOOL m_bAutoDelete; // enables 'delete this' after thread termination // only valid while running HANDLE m_hThread; // this thread's HANDLE operator HANDLE() const; DWORD m_nThreadID; // this thread's ID int GetThreadPriority(); BOOL SetThreadPriority(int nPriority); // Operations DWORD SuspendThread(); DWORD ResumeThread(); BOOL PostThreadMessage(UINT message, WPARAM wParam, LPARAM lParam); // Overridables //執行線程實例初始化,必須重寫 virtual BOOL InitInstance(); // running and idle processing //控制線程的函數,包含消息泵,一般不重寫 virtual int Run(); //消息調度到TranslateMessage和DispatchMessage之前對其進行篩選, //通常不重寫 virtual BOOL PreTranslateMessage(MSG* pMsg); virtual BOOL PumpMessage(); // low level message pump //執行線程特定的閑置時間處理,通常不重寫 virtual BOOL OnIdle(LONG lCount); // return TRUE if more idle processing virtual BOOL IsIdleMessage(MSG* pMsg); // checks for special messages //線程終止時執行清除,通常需要重寫 virtual int ExitInstance(); // default will 'delete this' //截獲由線程的消息和命令處理程序引發的未處理異常,通常不重寫 virtual LRESULT ProcessWndProcException(CException* e, const MSG* pMsg); // Advanced: handling messages sent to message filter hook virtual BOOL ProcessMessageFilter(int code, LPMSG lpMsg); // Advanced: virtual access to m_pMainWnd virtual CWnd* GetMainWnd(); // Implementation public: virtual ~CWinThread(); #ifdef _DEBUG virtual void AssertValid() const; virtual void Dump(CDumpContext& dc) const; int m_nDisablePumpCount; // Diagnostic trap to detect illegal re-entrancy #endif void CommonConstruct(); virtual void Delete(); // 'delete this' only if m_bAutoDelete == TRUE // message pump for Run MSG m_msgCur; // current message public: // constructor used by implementation of AfxBeginThread CWinThread(AFX_THREADPROC pfnThreadProc, LPVOID pParam); // valid after construction LPVOID m_pThreadParams; // generic parameters passed to starting function AFX_THREADPROC m_pfnThreadProc; // set after OLE is initialized void (AFXAPI* m_lpfnOleTermOrFreeLib)(BOOL, BOOL); COleMessageFilter* m_pMessageFilter; protected: CPoint m_ptCursorLast; // last mouse position UINT m_nMsgLast; // last mouse message BOOL DispatchThreadMessageEx(MSG* msg); // helper void DispatchThreadMessage(MSG* msg); // obsolete }; |
啟動UI線程的AfxBeginThread函數的原型為:
CWinThread *AfxBeginThread( //從CWinThread派生的類的 RUNTIME_CLASS CRuntimeClass *pThreadClass, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL ); |
我們可以方便地使用VC++ 6.0類向導定義一個繼承自CWinThread的用戶線程類。下面給出產生我們自定義的CWinThread子類CMyUIThread的方法。
打開VC++ 6.0類向導,在如下窗口中選擇Base Class類為CWinThread,輸入子類名為CMyUIThread,點擊"OK"按鈕后就產生了類CMyUIThread。
![]() |
其源代碼框架為:
/ // CMyUIThread thread class CMyUIThread : public CWinThread { DECLARE_DYNCREATE(CMyUIThread) protected: CMyUIThread(); // protected constructor used by dynamic creation // Attributes public: // Operations public: // Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(CMyUIThread) public: virtual BOOL InitInstance(); virtual int ExitInstance(); //}}AFX_VIRTUAL // Implementation protected: virtual ~CMyUIThread(); // Generated message map functions //{{AFX_MSG(CMyUIThread) // NOTE - the ClassWizard will add and remove member functions here. //}}AFX_MSG DECLARE_MESSAGE_MAP() }; / // CMyUIThread IMPLEMENT_DYNCREATE(CMyUIThread, CWinThread) CMyUIThread::CMyUIThread() {} CMyUIThread::~CMyUIThread() {} BOOL CMyUIThread::InitInstance() { // TODO: perform and per-thread initialization here return TRUE; } int CMyUIThread::ExitInstance() { // TODO: perform any per-thread cleanup here return CWinThread::ExitInstance(); } BEGIN_MESSAGE_MAP(CMyUIThread, CWinThread) //{{AFX_MSG_MAP(CMyUIThread) // NOTE - the ClassWizard will add and remove mapping macros here. //}}AFX_MSG_MAP END_MESSAGE_MAP() |
使用下列代碼就可以啟動這個UI線程:
CMyUIThread *pThread; pThread = (CMyUIThread*) AfxBeginThread( RUNTIME_CLASS(CMyUIThread) ); |
另外,我們也可以不用AfxBeginThread 創建線程,而是分如下兩步完成:
(1)調用線程類的構造函數創建一個線程對象;
(2)調用CWinThread::CreateThread函數來啟動該線程。
在線程自身內調用AfxEndThread函數可以終止該線程:
void AfxEndThread( UINT nExitCode //the exit code of the thread ); |
對于UI線程而言,如果消息隊列中放入了WM_QUIT消息,將結束線程。
關于UI線程和工作者線程的分配,最好的做法是:將所有與UI相關的操作放入主線程,其它的純粹的運算工作交給獨立的數個工作者線程。
候捷先生早些時間喜歡為MDI程序的每個窗口創建一個線程,他后來澄清了這個錯誤。因為如果為MDI程序的每個窗口都單獨創建一個線程,在窗口進行切換的時候,將進行線程的上下文切換!
深入淺出Win32多線程程序設計之綜合實例
本章我們將以工業控制和嵌入式系統中運用極為廣泛的串口通信為例講述多線程的典型應用。而網絡通信也是多線程應用最廣泛的領域之一,所以本章的最后一節也將對多線程網絡通信進行簡短的描述。
1.串口通信
在工業控制系統中,工控機(一般都基于PC Windows平臺)經常需要與單片機通過串口進行通信。因此,操作和使用PC的串口成為大多數單片機、嵌入式系統領域工程師必須具備的能力。
串口的使用需要通過三個步驟來完成的:
(1) 打開通信端口;
(2) 初始化串口,設置波特率、數據位、停止位、奇偶校驗等參數。為了給讀者一個直觀的印象,下圖從Windows的"控制面板->系統->設備管理器->通信端口(COM1)"打開COM的設置窗口:
(3) 讀寫串口。
在WIN32平臺下,對通信端口進行操作跟基本的文件操作一樣。
創建/打開COM資源
下列函數如果調用成功,則返回一個標識通信端口的句柄,否則返回-1:
HADLE CreateFile(PCTSTR lpFileName, //通信端口名,如"COM1" WORD dwDesiredAccess, //對資源的訪問類型 WORD dwShareMode, //指定共享模式,COM不能共享,該參數為0 PSECURITY_ATTRIBUTES lpSecurityAttributes, //安全描述符指針,可為NULL WORD dwCreationDisposition, //創建方式 WORD dwFlagsAndAttributes, //文件屬性,可為NULL HANDLE hTemplateFile //模板文件句柄,置為NULL ); |
獲得/設置COM屬性
下列函數可以獲得COM口的設備控制塊,從而獲得相關參數:
BOOL WINAPI GetCommState( HANDLE hFile, //標識通信端口的句柄 LPDCB lpDCB //指向一個設備控制塊(DCB結構)的指針 ); |
如果要調整通信端口的參數,則需要重新配置設備控制塊,再用WIN32 API SetCommState()函數進行設置:
BOOL SetCommState( HANDLE hFile, //標識通信端口的句柄 LPDCB lpDCB //指向一個設備控制塊(DCB結構)的指針 ); |
DCB結構包含了串口的各項參數設置,如下:
typedef struct _DCB { // dcb DWORD DCBlength; // sizeof(DCB) DWORD BaudRate; // current baud rate DWORD fBinary: 1; // binary mode, no EOF check DWORD fParity: 1; // enable parity checking DWORD fOutxCtsFlow: 1; // CTS output flow control DWORD fOutxDsrFlow: 1; // DSR output flow control DWORD fDtrControl: 2; // DTR flow control type DWORD fDsrSensitivity: 1; // DSR sensitivity DWORD fTXContinueOnXoff: 1; // XOFF continues Tx DWORD fOutX: 1; // XON/XOFF out flow control DWORD fInX: 1; // XON/XOFF in flow control DWORD fErrorChar: 1; // enable error replacement DWORD fNull: 1; // enable null stripping DWORD fRtsControl: 2; // RTS flow control DWORD fAbortOnError: 1; // abort reads/writes on error DWORD fDummy2: 17; // reserved WORD wReserved; // not currently used WORD XonLim; // transmit XON threshold WORD XoffLim; // transmit XOFF threshold BYTE ByteSize; // number of bits/byte, 4-8 BYTE Parity; // 0-4=no,odd,even,mark,space BYTE StopBits; // 0,1,2 = 1, 1.5, 2 char XonChar; // Tx and Rx XON character char XoffChar; // Tx and Rx XOFF character char ErrorChar; // error replacement character char EofChar; // end of input character char EvtChar; // received event character WORD wReserved1; // reserved; do not use } DCB; |
讀寫串口
在讀寫串口之前,還要用PurgeComm()函數清空緩沖區,并用SetCommMask ()函數設置事件掩模來監視指定通信端口上的事件,其原型為:
BOOL SetCommMask( HANDLE hFile, //標識通信端口的句柄 DWORD dwEvtMask //能夠使能的通信事件 ); |
串口上可能發生的事件如下表所示:
值 | 事件描述 |
EV_BREAK | A break was detected on input. |
EV_CTS | The CTS (clear-to-send) signal changed state. |
EV_DSR | The DSR(data-set-ready) signal changed state. |
EV_ERR | A line-status error occurred. Line-status errors are CE_FRAME, CE_OVERRUN, and CE_RXPARITY. |
EV_RING | A ring indicator was detected. |
EV_RLSD | The RLSD (receive-line-signal-detect) signal changed state. |
EV_RXCHAR | A character was received and placed in the input buffer. |
EV_RXFLAG | The event character was received and placed in the input buffer. The event character is specified in the device's DCB structure, which is applied to a serial port by using the SetCommState function. |
EV_TXEMPTY | The last character in the output buffer was sent. |
在設置好事件掩模后,我們就可以利用WaitCommEvent()函數來等待串口上發生事件,其函數原型為:
BOOL WaitCommEvent( HANDLE hFile, //標識通信端口的句柄 LPDWORD lpEvtMask, //指向存放事件標識變量的指針 LPOVERLAPPED lpOverlapped, // 指向overlapped結構 ); |
我們可以在發生事件后,根據相應的事件類型,進行串口的讀寫操作:
BOOL ReadFile(HANDLE hFile, //標識通信端口的句柄
LPVOID lpBuffer, //輸入數據Buffer指針
DWORD nNumberOfBytesToRead, // 需要讀取的字節數
LPDWORD lpNumberOfBytesRead, //實際讀取的字節數指針
LPOVERLAPPED lpOverlapped //指向overlapped結構
);
BOOL WriteFile(HANDLE hFile, //標識通信端口的句柄
LPCVOID lpBuffer, //輸出數據Buffer指針
DWORD nNumberOfBytesToWrite, //需要寫的字節數
LPDWORD lpNumberOfBytesWritten, //實際寫入的字節數指針
LPOVERLAPPED lpOverlapped //指向overlapped結構
);