第十二章 多線程與串行通信
Windows是一個多任務操作系統。傳統的Windows 3.x只能依靠應用程序之間的協同來實現協同式多任務,而Windows 95/NT實行的是搶先式多任務。
在Win 32(95/NT)中,每一個進程可以同時執行多個線程,這意味着一個程序可以同時完成多個任務。對於象通信程序這樣既要進行耗時的工作,又要保持對用戶輸入響應的應用來說,使用多線程是最佳選擇。當進程使用多個線程時,需要採取適當的措施來保持線程間的同步。
利用Win 32的重疊I/O操作和多線程特性,程序員可以編寫出高效的通信程序。在這一講的最後將通過一個簡單的串行通信程序,向讀者演示多線程和重疊I/O的編程技術。
12.1 多任務、進程和線程
12.1.1 Windows 3.x的協同多任務
在16位的Windows 3.x中,應用程序具有對CPU的控制權。只有在調用了GetMessage、PeekMessage、WaitMessage或Yield後,程序纔有可能把CPU控制權交給系統,系統再把控制權轉交給別的應用程序。如果應用程序在長時間內無法調用上述四個函數之一,那麼程序就一直獨佔CPU,系統會被掛起而無法接受用戶的輸入。
因此,在設計16位的應用程序時,程序員必須合理地設計消息處理函數,以使程序能夠儘快返回到消息循環中。如果程序需要進行費時的操作,那麼必須保證程序在進行操作時能週期性的調用上述四個函數中的一個。
在Windows 3.x環境下,要想設計一個既能執行實時的後臺工作(如對通信端口的實時監測和讀寫),又能保證所有界面響應用戶輸入的單獨的應用程序幾乎是不可能的。
有人可能會想到用CWinApp::OnIdle函數來執行後臺工作,因爲該函數是程序主消息循環在空閒時調用的。但OnIdle的執行並不可靠,例如,如果用戶在程序中打開了一個菜單或模態對話框,那麼OnIdle將停止調用,因爲此時程序不能返回到主消息循環中!在實時任務代碼中調用PeekMessage也會遇到同樣的問題,除非程序能保證用戶不會選擇菜單或彈出模態對話框,否則程序將不能返回到PeekMessage的調用處,這將導致後臺實時處理的中斷。
折衷的辦法是在執行長期工作時彈出一個非模態對話框並禁止主窗口,在消息循環內分批執行後臺操作。對話框中可以顯示工作的進度,也可以包含一個取消按鈕以讓用戶有機會中斷一個長期的工作。典型的代碼如清單12.1所示。這樣做既可以保證工作實時進行,又可以使程序能有限地響應用戶輸入,但此時程序實際上已不能再爲用戶幹別的事情了。
清單12.1 在協同多任務環境下防止程序被掛起的一種方法
bAbort=FALSE;
lpMyDlgProc=MakeProcInstance(MyDlgProc, hInst);
hMyDlg=CreateDialog(hInst, “Abort”, hwnd, lpMyDlgProc); //創建一個非模態對話框
ShowWindow(hMyDlg, SW_NORMAL);
UpdateWindow(hMyDlg);
EnableWindow(hwnd, FALSE); //禁止主窗口
. . .
while(!bAbort)
{
. . . //執行一次後臺操作
. . .
while(PeekMessage(&msg, NULL, NULL, NULL, PM_REMOVE))
{
if(!IsDialogMessage(hMyDlg, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
EnableWindow(hwnd, TRUE); //允許主窗口
DestroyWindow(hMyDlg);
FreeProcInstance(lpMyDlgProc);
12.1.2 Windows 95/NT的搶先式多任務
在32位的Windows系統中,採用的是搶先式多任務,這意味着程序對CPU的佔用時間是由系統決定的。系統爲每個程序分配一定的CPU時間,當程序的運行超過規定時間後,系統就會中斷該程序並把CPU控制權轉交給別的程序。與協同式多任務不同,這種中斷是彙編語言級的。程序不必調用象PeekMessage這樣的函數來放棄對CPU的控制權,就可以進行費時的工作,而且不會導致系統的掛起。
例如,在Windows3.x 中,如果某一個應用程序陷入了死循環,那麼整個系統都會癱瘓,這時唯一的解決辦法就是重新啓動機器。而在Windows 95/NT中,一個程序的崩潰一般不會造成死機,其它程序仍然可以運行,用戶可以按Ctrl+Alt+Del鍵來打開任務列表並關閉沒有響應的程序。
12.1.3 進程與線程
在32位的Windows系統中,術語多任務是指系統可以同時運行多個進程,而每個進程也可以同時執行多個線程。
進程就是應用程序的運行實例。每個進程都有自己私有的虛擬地址空間。每個進程都有一個主線程,但可以建立另外的線程。進程中的線程是並行執行的,每個線程佔用CPU的時間由系統來劃分。
可以把線程看成是操作系統分配CPU時間的基本實體。系統不停地在各個線程之間切換,它對線程的中斷是彙編語言級的。系統爲每一個線程分配一個CPU時間片,某個線程只有在分配的時間片內纔有對CPU的控制權。實際上,在PC機中,同一時間只有一個線程在運行。由於系統爲每個線程劃分的時間片很小(20毫秒左右),所以看上去好象是多個線程在同時運行。
進程中的所有線程共享進程的虛擬地址空間,這意味着所有線程都可以訪問進程的全局變量和資源。這一方面爲編程帶來了方便,但另一方面也容易造成衝突。
雖然在進程中進行費時的工作不會導致系統的掛起,但這會導致進程本身的掛起。所以,如果進程既要進行長期的工作,又要響應用戶的輸入,那麼它可以啓動一個線程來專門負責費時的工作,而主線程仍然可以與用戶進行交互。
12.1.4 線程的創建和終止
線程分用戶界面線程和工作者線程兩種。用戶界面線程擁有自己的消息泵來處理界面消息,可以與用戶進行交互。工作者線程沒有消息泵,一般用來完成後臺工作。
MFC應用程序的線程由對象CWinThread表示。在多數情況下,程序不需要自己創建CWinThread對象。調用AfxBeginThread函數時會自動創建一個CWinThread對象。
例如,清單12.2中的代碼演示了工作者線程的創建。AfxBeginThread函數負責創建新線程,它的第一個參數是代表線程的函數的地址,在本例中是MyThreadProc。第二個參數是傳遞給線程函數的參數,這裏假定線程要用到CMyObject對象,所以把pNewObject指針傳給了新線程。線程函數MyThreadProc用來執行線程,請注意該函數的聲明。線程函數有一個32位的pParam參數可用來接收必要的參數。
清單12.2 創建一個工作者線程
//主線程
pNewObject = new CMyObject;
AfxBeginThread(MyThreadProc, pNewObject);
//新線程
UINT MyThreadProc( LPVOID pParam )
{
CMyObject* pObject = (CMyObject*)pParam;
if (pObject == NULL ||
!pObject->IsKindOf(RUNTIME_CLASS(CMyObject)))
return -1; // 非法參數
// 用pObject對象來完成某項工作
return 0; // 線程正常結束
}
AfxBeginThread的聲明爲:
CWinThread* AfxBeginThread( AFX_THREADPROC pfnThreadProc, LPVOID pParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
參數pfnThreadProc是工作線程函數的地址。pParam是傳遞給線程函數的參數。nPriority是線程的優先級,一般是THREAD_PRIORITY_NORMAL,若爲0,則使用創建線程的優先級。nStackSize說明了線程的堆棧尺寸,若爲0則堆棧尺寸與創建線程相同。dwCreateFlags指定了線程的初始狀態,如果爲0,那麼線程在創建後立即執行,如果爲CREATE_SUSPENDED,則線程在創建後就被掛起。參數lpSecurityAttrs用來說明保密屬性,一般爲0。函數返回新建的CWinThread對象的指針。
程序應該把AfxBeginThread返回的CWinThread指針保存起來,以便對創建的線程進行控制。例如,可以調用CWinThread::SetThreadPriority來設置線程的優先級,用CWinThread::SuspendThread來掛起線程。如果線程被掛起,那麼直到調用CWinThread::ResumeThread後線程纔開始運行。
如果要創建用戶界面線程,那麼必須從CWinThread派生一個新類。事實上,代表進程主線程的CWinApp類就是CWinThread的派生類。派生類必須用DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE宏來聲明和實現。需要重寫派生類的InitInstance、ExitInstance、Run等函數。
可以使用AfxBeginThread函數的另一個版本來創建用戶界面線程。函數的聲明爲:
CWinThread* AfxBeginThread( CRuntimeClass* pThreadClass, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );
參數pThreadClass指向一個CRuntimeClass對象,該對象是用RUNTIME_CLASS宏從CWinThread的派生類創建的。其它參數以及函數的返回值與第一個版本的AfxBeginThread是一樣的。
當發生下列事件之一時,線程被終止:
線程調用ExitThread。
線程函數返回,即線程隱含調用了ExitThread。
ExitProcess被進程的任一線程顯示或隱含調用。
用線程的句柄調用TerminateThread。
用進程句柄調用TerminateProcess。
12.2 線程的同步
多線程的使用會產生一些新的問題,主要是如何保證線程的同步執行。多線程應用程序需要使用同步對象和等待函數來實現同步。
12.2.1 爲什麼需要同步
由於同一進程的所有線程共享進程的虛擬地址空間,並且線程的中斷是彙編語言級的,所以可能會發生兩個線程同時訪問同一個對象(包括全局變量、共享資源、API函數和MFC對象等)的情況,這有可能導致程序錯誤。例如,如果一個線程在未完成對某一大尺寸全局變量的讀操作時,另一個線程又對該變量進行了寫操作,那麼第一個線程讀入的變量值可能是一種修改過程中的不穩定值。
屬於不同進程的線程在同時訪問同一內存區域或共享資源時,也會存在同樣的問題。
因此,在多線程應用程序中,常常需要採取一些措施來同步線程的執行。需要同步的情況包括以下幾種:
在多個線程同時訪問同一對象時,可能產生錯誤。例如,如果當一個線程正在讀取一個至關重要的共享緩衝區時,另一個線程向該緩衝區寫入數據,那麼程序的運行結果就可能出錯。程序應該儘量避免多個線程同時訪問同一個緩衝區或系統資源。
在Windows 95環境下編寫多線程應用程序還需要考慮重入問題。Windows NT是真正的32位操作系統,它解決了系統重入問題。而Windows 95由於繼承了Windows 3.x的部分16位代碼,沒能夠解決重入問題。這意味着在Windows 95中兩個線程不能同時執行某個系統功能,否則有可能造成程序錯誤,甚至會造成系統崩潰。應用程序應該儘量避免發生兩個以上的線程同時調用同一個Windows API函數的情況。
由於大小和性能方面的原因,MFC對象在對象級不是線程安全的,只有在類級纔是。也就是說,兩個線程可以安全地使用兩個不同的CString對象,但同時使用同一個CString對象就可能產生問題。如果必須使用同一個對象,那麼應該採取適當的同步措施。
多個線程之間需要協調運行。例如,如果第二個線程需要等待第一個線程完成到某一步時才能運行,那麼該線程應該暫時掛起以減少對CPU的佔用時間,提高程序的執行效率。當第一個線程完成了相應的步驟後,應該發出某種信號來激活第二個線程。
12.2.2 等待函數
Win32 API提供了一組能使線程阻塞其自身執行的等待函數。這些函數只有在作爲其參數的一個或多個同步對象(見下小節)產生信號時纔會返回。在超過規定的等待時間後,不管有無信號,函數也都會返回。在等待函數未返回時,線程處於等待狀態,此時線程只消耗很少的CPU時間。
使用等待函數即可以保證線程的同步,又可以提高程序的運行效率。最常用的等待函數是WaitForSingleObject,該函數的聲明爲:
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
參數hHandle是同步對象的句柄。參數dwMilliseconds是以毫秒爲單位的超時間隔,如果該參數爲0,那麼函數就測試同步對象的狀態並立即返回,如果該參數爲INFINITE,則超時間隔是無限的。函數的返回值在表12.1中列出。
表12.1 WaitForSingleObject的返回值
返回值
含義
WAIT_FAILED
函數失敗
WAIT_OBJECT_0
指定的同步對象處於有信號的狀態
WAIT_ABANDONED
擁有一個mutex的線程已經中斷了,但未釋放該MUTEX
WAIT_TIMEOUT
超時返回,並且同步對象無信號
函數WaitForMultipleObjects可以同時監測多個同步對象,該函數的聲明爲:
DWORD WaitForMultipleObjects(DWORD nCount, CONST HANDLE *lpHandles, BOOL bWaitAll, DWORD dwMilliseconds );
參數nCount是句柄數組中句柄的數目。lpHandles代表一個句柄數組。bWaitAll說明了等待類型,如果爲TRUE,那麼函數在所有對象都有信號後才返回,如果爲FALSE,則只要有一個對象變成有信號的,函數就返回。函數的返回值在表12.2中列出。參數dwMilliseconds是以毫秒爲單位的超時間隔,如果該參數爲0,那麼函數就測試同步對象的狀態並立即返回,如果該參數爲INFINITE,則超時間隔是無限的。
表12.2 WaitForMultipleObjects的返回值
返回值
說明
WAIT_OBJECT_0到WAIT_ OBJECT_0+nCount-1
若bWaitAll爲TRUE,則返回值表明所有對象都是有信號的。如果bWaitAll爲FALSE,則返回值減去WAIT_OBJECT_0就是數組中有信號對象的最小索引。
WAIT_ABANDONED_0到WAIT_ ABANDONED_ 0+nCount-1
若bWaitAll爲TRUE,則返回值表明所有對象都有信號,但有一個mutex被放棄了。若bWaitAll爲FALSE,則返回值減去WAIT_ABANDONED_0就是被放棄mutex在對象數組中的索引。
WAIT_TIMEOUT
超時返回。
12.2.3 同步對象
同步對象用來協調多線程的執行,它可以被多個線程共享。線程的等待函數用同步對象的句柄作爲參數,同步對象應該是所有要使用的線程都能訪問到的。同步對象的狀態要麼是有信號的,要麼是無信號的。同步對象主要有三種:事件、mutex和信號燈。
事件對象(Event)是最簡單的同步對象,它包括有信號和無信號兩種狀態。在線程訪問某一資源之前,也許需要等待某一事件的發生,這時用事件對象最合適。例如,只有在通信端口緩衝區收到數據後,監視線程才被激活。
事件對象是用CreateEvent函數建立的。該函數可以指定事件對象的種類和事件的初始狀態。如果是手工重置事件,那麼它總是保持有信號狀態,直到用ResetEvent函數重置成無信號的事件。如果是自動重置事件,那麼它的狀態在單個等待線程釋放後會自動變爲無信號的。用SetEvent可以把事件對象設置成有信號狀態。在建立事件時,可以爲對象起個名字,這樣其它進程中的線程可以用OpenEvent函數打開指定名字的事件對象句柄。
mutex對象的狀態在它不被任何線程擁有時是有信號的,而當它被擁有時則是無信號的。mutex對象很適合用來協調多個線程對共享資源的互斥訪問(mutually exclusive)。
線程用CreateMutex函數來建立mutex對象,在建立mutex時,可以爲對象起個名字,這樣其它進程中的線程可以用OpenMutex函數打開指定名字的mutex對象句柄。在完成對共享資源的訪問後,線程可以調用ReleaseMutex來釋放mutex,以便讓別的線程能訪問共享資源。如果線程終止而不釋放mutex,則認爲該mutex被廢棄。
信號燈對象維護一個從0開始的計數,在計數值大於0時對象是有信號的,而在計數值爲0時則是無信號的。信號燈對象可用來限制對共享資源進行訪問的線程數量。線程用CreateSemaphore函數來建立信號燈對象,在調用該函數時,可以指定對象的初始計數和最大計數。在建立信號燈時也可以爲對象起個名字,別的進程中的線程可以用OpenSemaphore函數打開指定名字的信號燈句柄。
一般把信號燈的初始計數設置成最大值。每次當信號燈有信號使等待函數返回時,信號燈計數就會減1,而調用ReleaseSemaphore可以增加信號燈的計數。計數值越小就表明訪問共享資源的程序越多。
除了上述三種同步對象外,表12.3中的對象也可用於同步。另外,有時可以用文件或通信設備作爲同步對象使用。
表12.3 可用於同步的對象
對象
描述
變化通知
由FindFirstChangeNotification函數建立,當在指定目錄中發生指定類型的變化時對象變成有信號的。
控制檯輸入
在控制檯建立是被創建。它是用CONIN$調用CreateFile函數返回的句柄,或是GetStdHandle函數的返回句柄。如果控制檯輸入緩衝區中有數據,那麼對象是有信號的,如果緩衝區爲空,則對象是無信號的。
進程
當調用CreateProcess建立進程時被創建。進程在運行時對象是無信號的,當進程終止時對象是有信號的。
線程
當調用Createprocess、CreateThread或CreateRemoteThread函數創建新線程時被創建。在線程運行是對象是無信號的,在線程終止時則是有信號的。
當對象不再使用時,應該用CloseHandle函數關閉對象句柄。
清單12.3是一個使用事件對象的簡單例子,在該例中,假設主線程要讀取共享緩衝區中的內容,而輔助線程負責向緩衝區中寫入數據。兩個線程使用了一個hEvent事件對象來同步。在用CreateEvent函數創建事件對象句柄時,指定該對象是一個自動重置事件,其初始狀態爲有信號的。當線程要讀寫緩衝區時,調用WaitForSingleObject函數無限等待hEvent信號。如果hEvent無信號,則說明另一線程正在訪問緩衝區;如果有信號,則本線程可以訪問緩衝區,WaitForSingleObject函數在返回後會自動把hEvent置成無信號的,這樣在本線程讀寫緩衝區時別的線程不會同時訪問。在完成讀寫操作後,調用SetEvent函數把hEvent置成有信號的,以使別的線程有機會訪問共享緩衝區。
清單12.3 使用事件對象的簡單例子
HANDLE hEvent; //全局變量
//主線程
hEvent=CreateEvent(NULL, FALSE, TRUE, NULL);
if(hEvent= =NULL) return;
. . .
WaitForSingleObject(hEvent, INFINITE);
ReadFromBuf( );
SetEvent( hEvent );
. . .
CloseHandle( hEvent );
//輔助線程
UINT MyThreadProc( LPVOID pParam )
{
. . .
WaitForSingleObject(hEvent, INFINITE);
WriteToBuf( );
SetEvent( hEvent );
. . .
return 0; // 線程正常結束
}
12.2.4 關鍵節和互鎖變量訪問
關鍵節(Critical Seciton)與mutex的功能類似,但它只能由同一進程中的線程使用。關鍵節可以防止共享資源被同時訪問。
進程負責爲關鍵節分配內存空間,關鍵節實際上是一個CRITICAL_SECTION型的變量,它一次只能被一個線程擁有。在線程使用關鍵節之前,必須調用InitializeCriticalSection函數將其初始化。如果線程中有一段關鍵的代碼不希望被別的線程中斷,那麼可以調用EnterCriticalSection函數來申請關鍵節的所有權,在運行完關鍵代碼後再用LeaveCriticalSection函數來釋放所有權。如果在調用EnterCriticalSection時關鍵節對象已被另一個線程擁有,那麼該函數將無限期等待所有權。
利用互鎖變量可以建立簡單有效的同步機制。使用函數InterlockedIncrement和InterlockedDecrement可以增加或減少多個線程共享的一個32位變量的值,並且可以檢查結果是否爲0。線程不必擔心會被其它線程中斷而導致錯誤。如果變量位於共享內存中,那麼不同進程中的線程也可以使用這種機制。
12.3 串行通信與重疊I/O
Win 32系統爲串行通信提供了全新的服務。傳統的OpenComm、ReadComm、WriteComm、CloseComm等函數已經過時,WM_COMMNOTIFY消息也消失了。取而代之的是文件I/O函數提供的打開和關閉通信資源句柄及讀寫操作的基本接口。
新的文件I/O函數(CreateFile、ReadFile、WriteFile等)支持重疊式輸入輸出,這使得線程可以從費時的I/O操作中解放出來,從而極大地提高了程序的運行效率。
12.3.1 串行口的打開和關閉
Win 32系統把文件的概念進行了擴展。無論是文件、通信設備、命名管道、郵件槽、磁盤、還是控制檯,都是用API函數CreateFile來打開或創建的。該函數的聲明爲:
HANDLE CreateFile(
LPCTSTR lpFileName, // 文件名
DWORD dwDesiredAccess, // 訪問模式
DWORD dwShareMode, // 共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 通常爲NULL
DWORD dwCreationDistribution, // 創建方式
DWORD dwFlagsAndAttributes, // 文件屬性和標誌
HANDLE hTemplateFile // 臨時文件的句柄,通常爲NULL
);
如果調用成功,那麼該函數返回文件的句柄,如果調用失敗,則函數返回INVALID_HANDLE_VALUE。
如果想要用重疊I/O方式(參見12.3.3)打開COM2口,則一般應象清單12.4那樣調用CreateFile函數。注意在打開一個通信端口時,應該以獨佔方式打開,另外要指定GENERIC_READ、GENERIC_WRITE、OPEN_EXISTING和FILE_ATTRIBUTE_NORMAL等屬性。如果要打開重疊I/O,則應該指定 FILE_FLAG_OVERLAPPED屬性。
清單12.4
HANDLE hCom;
DWORD dwError;
hCom=CreateFile(“COM2”, // 文件名
GENERIC_READ | GENERIC_WRITE, // 允許讀和寫
0, // 獨佔方式
NULL,
OPEN_EXISTING, //打開而不是創建
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // 重疊方式
NULL
);
if(hCom = = INVALID_HANDLE_VALUE)
{
dwError=GetLastError( );
. . . // 處理錯誤
}
當不再使用文件句柄時,應該調用CloseHandle函數關閉之。
12.3.2 串行口的初始化
在打開通信設備句柄後,常常需要對串行口進行一些初始化工作。這需要通過一個DCB結構來進行。DCB結構包含了諸如波特率、每個字符的數據位數、奇偶校驗和停止位數等信息。在查詢或配置置串行口的屬性時,都要用DCB結構來作爲緩衝區。
調用GetCommState函數可以獲得串口的配置,該函數把當前配置填充到一個DCB結構中。一般在用CreateFile打開串行口後,可以調用GetCommState函數來獲取串行口的初始配置。要修改串行口的配置,應該先修改DCB結構,然後再調用SetCommState函數用指定的DCB結構來設置串行口。
除了在DCB中的設置外,程序一般還需要設置I/O緩衝區的大小和超時。Windows用I/O緩衝區來暫存串行口輸入和輸出的數據,如果通信的速率較高,則應該設置較大的緩衝區。調用SetupComm函數可以設置串行口的輸入和輸出緩衝區的大小。
在用ReadFile和WriteFile讀寫串行口時,需要考慮超時問題。如果在指定的時間內沒有讀出或寫入指定數量的字符,那麼ReadFile或WriteFile的操作就會結束。要查詢當前的超時設置應調用GetCommTimeouts函數,該函數會填充一個COMMTIMEOUTS結構。調用SetCommTimeouts可以用某一個COMMTIMEOUTS結構的內容來設置超時。
有兩種超時:間隔超時和總超時。間隔超時是指在接收時兩個字符之間的最大時延,總超時是指讀寫操作總共花費的最大時間。寫操作只支持總超時,而讀操作兩種超時均支持。用COMMTIMEOUTS結構可以規定讀/寫操作的超時,該結構的定義爲:
typedef struct _COMMTIMEOUTS {
DWORD ReadIntervalTimeout; // 讀間隔超時
DWORD ReadTotalTimeoutMultiplier; // 讀時間係數
DWORD ReadTotalTimeoutConstant; // 讀時間常量
DWORD WriteTotalTimeoutMultiplier; // 寫時間係數
DWORD WriteTotalTimeoutConstant; // 寫時間常量
} COMMTIMEOUTS,*LPCOMMTIMEOUTS;
COMMTIMEOUTS結構的成員都以毫秒爲單位。總超時的計算公式是:
總超時=時間係數×要求讀/寫的字符數 + 時間常量
例如,如果要讀入10個字符,那麼讀操作的總超時的計算公式爲:
讀總超時=ReadTotalTimeoutMultiplier×10 + ReadTotalTimeoutConstant
可以看出,間隔超時和總超時的設置是不相關的,這可以方便通信程序靈活地設置各種超時。
如果所有寫超時參數均爲0,那麼就不使用寫超時。如果ReadIntervalTimeout爲0,那麼就不使用讀間隔超時,如果ReadTotalTimeoutMultiplier和ReadTotalTimeoutConstant都爲0,則不使用讀總超時。如果讀間隔超時被設置成MAXDWORD並且兩個讀總超時爲0,那麼在讀一次輸入緩衝區中的內容後讀操作就立即完成,而不管是否讀入了要求的字符。
在用重疊方式讀寫串行口時,雖然ReadFile和WriteFile在完成操作以前就可能返回,但超時仍然是起作用的。在這種情況下,超時規定的是操作的完成時間,而不是ReadFile和WriteFile的返回時間。
清單12.5列出了一段簡單的串行口初始化代碼。
清單12.5 打開並初始化串行口
HANDLE hCom;
DWORD dwError;
DCB dcb;
COMMTIMEOUTS TimeOuts;
hCom=CreateFile(“COM2”, // 文件名
GENERIC_READ | GENERIC_WRITE, // 允許讀和寫
0, // 獨佔方式
NULL,
OPEN_EXISTING, //打開而不是創建
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // 重疊方式
NULL
);
if(hCom = = INVALID_HANDLE_VALUE)
{
dwError=GetLastError( );
. . . // 處理錯誤
}
SetupComm( hCom, 1024, 1024 ) //緩衝區的大小爲1024
TimeOuts. ReadIntervalTimeout=1000;
TimeOuts.ReadTotalTimeoutMultiplier=500;
TimeOuts.ReadTotalTimeoutConstant=5000;
TimeOuts.WriteTotalTimeoutMultiplier=500;
TimeOuts.WriteTotalTimeoutConstant=5000;
SetCommTimeouts(hCom, &TimeOuts); // 設置超時
GetCommState(hCom, &dcb);
dcb.BaudRate=2400; // 波特率爲2400
dcb.ByteSize=8; // 每個字符有8位
dcb.Parity=NOPARITY; //無校驗
dcb.StopBits=ONESTOPBIT; //一個停止位
SetCommState(hCom, &dcb);
12.3.3 重疊I/O
在用ReadFile和WriteFile讀寫串行口時,既可以同步執行,也可以重疊(異步)執行。在同步執行時,函數直到操作完成後才返回。這意味着在同步執行時線程會被阻塞,從而導致效率下降。在重疊執行時,即使操作還未完成,調用的函數也會立即返回。費時的I/O操作在後臺進行,這樣線程就可以幹別的事情。例如,線程可以在不同的句柄上同時執行I/O操作,甚至可以在同一句柄上同時進行讀寫操作。“重疊”一詞的含義就在於此。
ReadFile函數只要在串行口輸入緩衝區中讀入指定數量的字符,就算完成操作。而WriteFile函數不但要把指定數量的字符拷入到輸出緩衝中,而且要等這些字符從串行口送出去後纔算完成操作。
ReadFile和WriteFile函數是否爲執行重疊操作是由CreateFile函數決定的。如果在調用CreateFile創建句柄時指定了FILE_FLAG_OVERLAPPED標誌,那麼調用ReadFile和WriteFile對該句柄進行的讀寫操作就是重疊的,如果未指定重疊標誌,則讀寫操作是同步的。
函數ReadFile和WriteFile的參數和返回值很相似。這裏僅列出ReadFile函數的聲明:
BOOL ReadFile(
HANDLE hFile, // 文件句柄
LPVOID lpBuffer, // 讀緩衝區
DWORD nNumberOfBytesToRead, // 要求讀入的字節數
LPDWORD lpNumberOfBytesRead, // 實際讀入的字節數
LPOVERLAPPED lpOverlapped // 指向一個OVERLAPPED結構
); //若返回TRUE則表明操作成功
需要注意的是如果該函數因爲超時而返回,那麼返回值是TRUE。參數lpOverlapped在重疊操作時應該指向一個OVERLAPPED結構,如果該參數爲NULL,那麼函數將進行同步操作,而不管句柄是否是由FILE_FLAG_OVERLAPPED標誌建立的。
當ReadFile和WriteFile返回FALSE時,不一定就是操作失敗,線程應該調用GetLastError函數分析返回的結果。例如,在重疊操作時如果操作還未完成函數就返回,那麼函數就返回FALSE,而且GetLastError函數返回ERROR_IO_PENDING。
在使用重疊I/O時,線程需要創建OVERLAPPED結構以供讀寫函數使用。OVERLAPPED結構最重要的成員是hEvent,hEvent是一個事件對象句柄,線程應該用CreateEvent函數爲hEvent成員創建一個手工重置事件,hEvent成員將作爲線程的同步對象使用。如果讀寫函數未完成操作就返回,就那麼把hEvent成員設置成無信號的。操作完成後(包括超時),hEvent會變成有信號的。
如果GetLastError函數返回ERROR_IO_PENDING,則說明重疊操作還爲完成,線程可以等待操作完成。有兩種等待辦法:一種辦法是用象WaitForSingleObject這樣的等待函數來等待OVERLAPPED結構的hEvent成員,可以規定等待的時間,在等待函數返回後,調用GetOverlappedResult。另一種辦法是調用GetOverlappedResult函數等待,如果指定該函數的bWait參數爲TRUE,那麼該函數將等待OVERLAPPED結構的hEvent 事件。GetOverlappedResult可以返回一個OVERLAPPED結構來報告包括實際傳輸字節在內的重疊操作結果。
如果規定了讀/寫操作的超時,那麼當超過規定時間後,hEvent成員會變成有信號的。因此,在超時發生後,WaitForSingleObject和GetOverlappedResult都會結束等待。WaitForSingleObject的dwMilliseconds參數會規定一個等待超時,該函數實際等待的時間是兩個超時的最小值。注意GetOverlappedResult不能設置等待的時限,因此如果hEvent成員無信號,則該函數將一直等待下去。
在調用ReadFile和WriteFile之前,線程應該調用ClearCommError函數清除錯誤標誌。該函數負責報告指定的錯誤和設備的當前狀態。
調用PurgeComm函數可以終止正在進行的讀寫操作,該函數還會清除輸入或輸出緩衝區中的內容。
12.3.4 通信事件
在Windows 95/NT中,WM_COMMNOTIFY消息已經取消,在串行口產生一個通信事件時,程序並不會收到通知消息。線程需要調用WaitCommEvent函數來監視發生在串行口中的各種事件,該函數的第二個參數返回一個事件屏蔽變量,用來指示事件的類型。線程可以用SetCommMask建立事件屏蔽以指定要監視的事件,表12.4列出了可以監視的事件。調用GetCommMask可以查詢串行口當前的事件屏蔽。
表12.4 通信事件
事件屏蔽
含義
EV_BREAK
檢測到一個輸入中斷
EV_CTS
CTS信號發生變化
EV_DSR
DSR信號發生變化
EV_ERR
發生行狀態錯誤
EV_RING
檢測到振鈴信號
EV_RLSD
RLSD(CD)信號發生變化
EV_RXCHAR
輸入緩衝區接收到新字符
EV_RXFLAG
輸入緩衝區收到事件字符
EV_TXEMPTY
發送緩衝區爲空
WaitCommEvent即可以同步使用,也可以重疊使用。如果串口是用FILE_FLAG_OVERLAPPED標誌打開的,那麼WaitCommEvent就進行重疊操作,此時該函數需要一個OVERLAPPED結構。線程可以調用等待函數或GetOverlappedResult函數來等待重疊操作的完成。
當指定範圍內的某一事件發生後,線程就結束等待並把該事件的屏蔽碼設置到事件屏蔽變量中。需要注意的是,WaitCommEvent只檢測調用該函數後發生的事件。例如,如果在調用WaitCommEvent前在輸入緩衝區中就有字符,則不會因爲這些字符而產生EV_RXCHAR事件。
如果檢測到輸入的硬件信號(如CTS、RTS和CD信號等)發生了變化,線程可以調用GetCommMaskStatus函數來查詢它們的狀態。而用EscapeCommFunction函數可以控制輸出的硬件信號(如DTR和RTS信號)。
12. 4 一個通信演示程序
爲了使讀者更好地掌握本章的概念,這裏舉一個具體實例來說明問題。如圖12.1所示,例子程序名爲Terminal,是一個簡單的TTY終端仿真程序。讀者可以用該程序打開一個串行口,該程序會把用戶的鍵盤輸入發送給串行口,並把從串口接收到的字符顯示在視圖中。用戶通過選擇File->Connect命令來打開串行口,選擇File->Disconnect命令則關閉串行口。
圖12.1 Terminal終端仿真程序
當用戶選擇File->Settings...命令時,會彈出一個Communication settings對話框,如圖12.2所示。該對話框主要用來設置串行口,包括端口、波特率、每字節位數、校驗、停止位數和流控制。
圖12.2 Communication settings對話框
通過該對話框也可以設置TTY終端仿真的屬性,如果選擇New Line(自動換行),那麼每當從串口讀到回車符(‘/r’)時,視圖中的正文就會換行,否則,只有在讀到換行符(‘/n’)時纔會換行。如果選擇Local echo(本地回顯),那麼發送的字符會在視圖中顯示出來。
終端仿真程序的特點是數據的傳輸沒有規律。因爲鍵盤輸入速度有限,所以發送的數據量較小,但接收的數據源是不確定的,所以有可能會有大量數據高速涌入的情況發生。根據Terminal的這些特性,我們在程序中創建了一個輔助工作者線程專門來監視串行口的輸入。由於寫入串行口的數據量不大,不會太費時,所以在主線程中完成寫端口的任務是可以的,不必另外創建線程。
現在就讓我們開始工作。請讀者按下面幾步進行:
用AppWizard建立一個名爲Terminal的MFC應用程序。在MFC AppWizard對話框的第1步選擇Single document,在第4步去掉Docking toolbar的選擇,在第6步把CTerminalView的基類改爲CEditView。
在Terminal工程的資源視圖中打開IDR_MAINFRAME菜單資源。去掉Edit菜單和View菜單,並去掉File菜單中除Exit以外的所有菜單項。然後在File菜單中加入三個菜單項,如表12.5所示。
表12.5 新菜單項
標題
ID
Settings...
ID_FILE_SETTINGS
Connect
ID_FILE_CONNECT
Disconnect
ID_FILE_DISCONNECT
用ClassWizard爲CTerminalDoc類創建三個與上表菜單消息對應的命令處理函數,使用缺省的函數名。爲ID_FILE_CONNECT和ID_FILE_DISCONNECT命令創建命令更新處理函數。另外,用ClassWizard爲該類加入CanCloseFrame成員函數。
用ClassWizard爲CTerminalView類創建OnChar函數,該函數用來把用戶鍵入的字符向串行口輸出。
新建一個對話框模板資源,令其ID爲IDD_COMSETTINGS。請按圖12.2和表12.6設計對話框模板。
表12.6 通信設置對話框中的主要控件
控件
ID
屬性設置
Base options組框
缺省
標題爲Base options
Port組合框
IDC_PORT
Drop List,不選Sort,初始列表爲COM1、COM2、COM3、COM4
Baud rate組合框
IDC_BAUD
Drop List,不選Sort,初始列表爲300、600、1200、2400、9600、14400、19200、38400、57600
Data bits組合框
IDC_DATABITS
Drop List,不選Sort,初列表爲5、6、7、8
Parity組合框
IDC_PARITY
Drop List,不選Sort,初列表爲None、Even、Odd
Stop bits組合框
IDC_STOPBITS
Drop List,不選Sort,初列表爲1、1.5、2
Flow control組框
缺省
標題爲Flow control
None單選按鈕
IDC_FLOWCTRL
標題爲None,選擇Group屬性
RTS/CTS單選按鈕
缺省
標題爲RTS/CTS
XON/XOFF單選按鈕
缺省
標題爲XON/XOFF
TTY options組框
缺省
標題爲TTY options
New line檢查框
IDC_NEWLINE
標題爲New line
Local echo檢查框
IDC_ECHO
標題爲Local echo
打開ClassWizard,爲IDD_COMSETTINGS模板創建一個名爲CSetupDlg的對話框類。爲該類加入OnInitDialog成員函數,並按表12.7加入數據成員。
表12.7 CSetupDlg類的數據成員
控件ID
變量名
數據類型
IDC_BAND
m_sBaud
CString
IDC_DATABITS
m_sDataBits
CString
IDC_ECHO
m_bEcho
BOOL
IDC_FLOWCTRL
m_nFlowCtrl
int
IDC_NEWLINE
m_bNewLine
BOOL
IDC_PARITY
m_nParity
int
IDC_PORT
m_sPort
CString
IDC_STOPBITS
m_nStopBits
int
按清單12.6、12.7和12.8修改程序。清單12.6列出了CTerminalDoc類的部分代碼,清單12.7是CTerminalView的部分代碼,清單12.8是CSetupDlg類的部分代碼。在本例中使用了WM_COMMNOTIFY消息。雖然在Win32中,WM_COMMNOTIFY消息已經取消,系統自己不會產生該消息,但Visual C++對該消息的定義依然保留。考慮到使用習慣,Terminal程序輔助線程通過發送該消息來通知視圖有通信事件發生。
清單12.6 CTerminalDoc類的部分代碼
// TerminalDoc.h : interface of the CTerminalDoc class
//
/////////////////////////////////////////////////////////////////////////////
#define MAXBLOCK 2048
#define XON 0x11
#define XOFF 0x13
UINT CommProc(LPVOID pParam);
class CTerminalDoc : public CDocument
{
protected: // create from serialization only
CTerminalDoc();
DECLARE_DYNCREATE(CTerminalDoc)
// Attributes
public:
CWinThread* m_pThread; // 代表輔助線程
volatile BOOL m_bConnected;
volatile HWND m_hTermWnd;
volatile HANDLE m_hPostMsgEvent; // 用於WM_COMMNOTIFY消息的事件對象
OVERLAPPED m_osRead, m_osWrite; // 用於重疊讀/寫
volatile HANDLE m_hCom; // 串行口句柄
int m_nBaud;
int m_nDataBits;
BOOL m_bEcho;
int m_nFlowCtrl;
BOOL m_bNewLine;
int m_nParity;
CString m_sPort;
int m_nStopBits;
// Operations
public:
BOOL ConfigConnection();
BOOL OpenConnection();
void CloseConnection();
DWORD ReadComm(char *buf,DWORD dwLength);
DWORD WriteComm(char *buf,DWORD dwLength);
// Overrides
. . .
};
/////////////////////////////////////////////////////////////////////////////
// TerminalDoc.cpp : implementation of the CTerminalDoc class
//
#include "SetupDlg.h"
CTerminalDoc::CTerminalDoc()
{
// TODO: add one-time construction code here
m_bConnected=FALSE;
m_pThread=NULL;
m_nBaud = 9600;
m_nDataBits = 8;
m_bEcho = FALSE;
m_nFlowCtrl = 0;
m_bNewLine = FALSE;
m_nParity = 0;
m_sPort = "COM2";
m_nStopBits = 0;
}
CTerminalDoc::~CTerminalDoc()
{
if(m_bConnected)
CloseConnection();
// 刪除事件句柄
if(m_hPostMsgEvent)
CloseHandle(m_hPostMsgEvent);
if(m_osRead.hEvent)
CloseHandle(m_osRead.hEvent);
if(m_osWrite.hEvent)
CloseHandle(m_osWrite.hEvent);
}
BOOL CTerminalDoc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
((CEditView*)m_viewList.GetHead())->SetWindowText(NULL);
// TODO: add reinitialization code here
// (SDI documents will reuse this document)
// 爲WM_COMMNOTIFY消息創建事件對象,手工重置,初始化爲有信號的
if((m_hPostMsgEvent=CreateEvent(NULL, TRUE, TRUE, NULL))==NULL)
return FALSE;
memset(&m_osRead, 0, sizeof(OVERLAPPED));
memset(&m_osWrite, 0, sizeof(OVERLAPPED));
// 爲重疊讀創建事件對象,手工重置,初始化爲無信號的
if((m_osRead.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL))==NULL)
return FALSE;
// 爲重疊寫創建事件對象,手工重置,初始化爲無信號的
if((m_osWrite.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL))==NULL)
return FALSE;
return TRUE;
}
void CTerminalDoc::OnFileConnect()
{
// TODO: Add your command handler code here
if(!OpenConnection())
AfxMessageBox("Can't open connection");
}
void CTerminalDoc::OnFileDisconnect()
{
// TODO: Add your command handler code here
CloseConnection();
}
void CTerminalDoc::OnUpdateFileConnect(CCmdUI* pCmdUI)
{
// TODO: Add your command update UI handler code here
pCmdUI->Enable(!m_bConnected);
}
void CTerminalDoc::OnUpdateFileDisconnect(CCmdUI* pCmdUI)
{
// TODO: Add your command update UI handler code here
pCmdUI->Enable(m_bConnected);
}
// 打開並配置串行口,建立工作者線程
BOOL CTerminalDoc::OpenConnection()
{
COMMTIMEOUTS TimeOuts;
POSITION firstViewPos;
CView *pView;
firstViewPos=GetFirstViewPosition();
pView=GetNextView(firstViewPos);
m_hTermWnd=pView->GetSafeHwnd();
if(m_bConnected)
return FALSE;
m_hCom=CreateFile(m_sPort, GENERIC_READ | GENERIC_WRITE, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL); // 重疊方式
if(m_hCom==INVALID_HANDLE_VALUE)
return FALSE;
SetupComm(m_hCom,MAXBLOCK,MAXBLOCK);
SetCommMask(m_hCom, EV_RXCHAR);
// 把間隔超時設爲最大,把總超時設爲0將導致ReadFile立即返回並完成操作
TimeOuts.ReadIntervalTimeout=MAXDWORD;
TimeOuts.ReadTotalTimeoutMultiplier=0;
TimeOuts.ReadTotalTimeoutConstant=0;
/* 設置寫超時以指定WriteComm成員函數中的
GetOverlappedResult函數的等待時間*/
TimeOuts.WriteTotalTimeoutMultiplier=50;
TimeOuts.WriteTotalTimeoutConstant=2000;
SetCommTimeouts(m_hCom, &TimeOuts);
if(ConfigConnection())
{
m_pThread=AfxBeginThread(CommProc, this, THREAD_PRIORITY_NORMAL,
0, CREATE_SUSPENDED, NULL); // 創建並掛起線程
if(m_pThread==NULL)
{
CloseHandle(m_hCom);
return FALSE;
}
else
{
m_bConnected=TRUE;
m_pThread->ResumeThread(); // 恢復線程運行
}
}
else
{
CloseHandle(m_hCom);
return FALSE;
}
return TRUE;
}
// 結束工作者線程,關閉串行口
void CTerminalDoc::CloseConnection()
{
if(!m_bConnected) return;
m_bConnected=FALSE;
//結束CommProc線程中WaitSingleObject函數的等待
SetEvent(m_hPostMsgEvent);
//結束CommProc線程中WaitCommEvent的等待
SetCommMask(m_hCom, 0);
//等待輔助線程終止
WaitForSingleObject(m_pThread->m_hThread, INFINITE);
m_pThread=NULL;
CloseHandle(m_hCom);
}
// 讓用戶設置串行口
void CTerminalDoc::OnFileSettings()
{
// TODO: Add your command handler code here
CSetupDlg dlg;
CString str;
dlg.m_bConnected=m_bConnected;
dlg.m_sPort=m_sPort;
str.Format("%d",m_nBaud);
dlg.m_sBaud=str;
str.Format("%d",m_nDataBits);
dlg.m_sDataBits=str;
dlg.m_nParity=m_nParity;
dlg.m_nStopBits=m_nStopBits;
dlg.m_nFlowCtrl=m_nFlowCtrl;
dlg.m_bEcho=m_bEcho;
dlg.m_bNewLine=m_bNewLine;
if(dlg.DoModal()==IDOK)
{
m_sPort=dlg.m_sPort;
m_nBaud=atoi(dlg.m_sBaud);
m_nDataBits=atoi(dlg.m_sDataBits);
m_nParity=dlg.m_nParity;
m_nStopBits=dlg.m_nStopBits;
m_nFlowCtrl=dlg.m_nFlowCtrl;
m_bEcho=dlg.m_bEcho;
m_bNewLine=dlg.m_bNewLine;
if(m_bConnected)
if(!ConfigConnection())
AfxMessageBox("Can't realize the settings!");
}
}
// 配置串行口
BOOL CTerminalDoc::ConfigConnection()
{
DCB dcb;
if(!GetCommState(m_hCom, &dcb))
return FALSE;
dcb.fBinary=TRUE;
dcb.BaudRate=m_nBaud; // 波特率
dcb.ByteSize=m_nDataBits; // 每字節位數
dcb.fParity=TRUE;
switch(m_nParity) // 校驗設置
{
case 0: dcb.Parity=NOPARITY;
break;
case 1: dcb.Parity=EVENPARITY;
break;
case 2: dcb.Parity=ODDPARITY;
break;
default:;
}
switch(m_nStopBits) // 停止位
{
case 0: dcb.StopBits=ONESTOPBIT;
break;
case 1: dcb.StopBits=ONE5STOPBITS;
break;
case 2: dcb.StopBits=TWOSTOPBITS;
break;
default:;
}
// 硬件流控制設置
dcb.fOutxCtsFlow=m_nFlowCtrl==1;
dcb.fRtsControl=m_nFlowCtrl==1?
RTS_CONTROL_HANDSHAKE:RTS_CONTROL_ENABLE;
// XON/XOFF流控制設置
dcb.fInX=dcb.fOutX=m_nFlowCtrl==2;
dcb.XonChar=XON;
dcb.XoffChar=XOFF;
dcb.XonLim=50;
dcb.XoffLim=50;
return SetCommState(m_hCom, &dcb);
}
// 從串行口輸入緩衝區中讀入指定數量的字符
DWORD CTerminalDoc::ReadComm(char *buf,DWORD dwLength)
{
DWORD length=0;
COMSTAT ComStat;
DWORD dwErrorFlags;
ClearCommError(m_hCom,&dwErrorFlags,&ComStat);
length=min(dwLength, ComStat.cbInQue);
ReadFile(m_hCom,buf,length,&length,&m_osRead);
return length;
}
// 將指定數量的字符從串行口輸出
DWORD CTerminalDoc::WriteComm(char *buf,DWORD dwLength)
{
BOOL fState;
DWORD length=dwLength;
COMSTAT ComStat;
DWORD dwErrorFlags;
ClearCommError(m_hCom,&dwErrorFlags,&ComStat);
fState=WriteFile(m_hCom,buf,length,&length,&m_osWrite);
if(!fState){
if(GetLastError()==ERROR_IO_PENDING)
{
GetOverlappedResult(m_hCom,&m_osWrite,&length,TRUE);// 等待
}
else
length=0;
}
return length;
}
// 工作者線程,負責監視串行口
UINT CommProc(LPVOID pParam)
{
OVERLAPPED os;
DWORD dwMask, dwTrans;
COMSTAT ComStat;
DWORD dwErrorFlags;
CTerminalDoc *pDoc=(CTerminalDoc*)pParam;
memset(&os, 0, sizeof(OVERLAPPED));
os.hEvent=CreateEvent(NULL, TRUE, FALSE, NULL);
if(os.hEvent==NULL)
{
AfxMessageBox("Can't create event object!");
return (UINT)-1;
}
while(pDoc->m_bConnected)
{
ClearCommError(pDoc->m_hCom,&dwErrorFlags,&ComStat);
if(ComStat.cbInQue)
{
// 無限等待WM_COMMNOTIFY消息被處理完
WaitForSingleObject(pDoc->m_hPostMsgEvent, INFINITE);
ResetEvent(pDoc->m_hPostMsgEvent);
// 通知視圖
PostMessage(pDoc->m_hTermWnd, WM_COMMNOTIFY, EV_RXCHAR, 0);
continue;
}
dwMask=0;
if(!WaitCommEvent(pDoc->m_hCom, &dwMask, &os)) // 重疊操作
{
if(GetLastError()==ERROR_IO_PENDING)
// 無限等待重疊操作結果
GetOverlappedResult(pDoc->m_hCom, &os, &dwTrans, TRUE);
else
{
CloseHandle(os.hEvent);
return (UINT)-1;
}
}
}
CloseHandle(os.hEvent);
return 0;
}
BOOL CTerminalDoc::CanCloseFrame(CFrameWnd* pFrame)
{
// TODO: Add your specialized code here and/or call the base class
SetModifiedFlag(FALSE); // 將文檔的修改標誌設置成未修改
return CDocument::CanCloseFrame(pFrame);
}
毫無疑問,CTerminalDoc類是研究重點。該類負責Terminal的通信任務,主要包括設置通信參數、打開和關閉串行口、建立和終止輔助工作線程、用輔助線程監視串行口等等。
在CTerminalDoc類的頭文件中,有些變量是用volatile關鍵字聲明的。當兩個線程都要用到某一個變量且該變量的值會被改變時,應該用volatile聲明,該關鍵字的作用是防止優化編譯器把變量從內存裝入CPU寄存器中。如果變量被裝入寄存器,那麼兩個線程有可能一個使用內存中的變量,一個使用寄存器中的變量,這會造成程序的錯誤執行。
成員m_bConnected用來表明當前是否存在一個通信連接。m_hTermWnd用來保存是視圖的窗口句柄。m_hPostMsgEvent事件對象用於WM_COMMNOTIFY消息的允許和禁止。m_pThread用來指向AfxBeginThread創建的CWinThread對象,以便對線程進行控制。OVERLAPPED結構m_osRead和m_osWrite用於串行口的重疊讀/寫,程序應該爲它們的hEvent成員創建事件句柄。
CTerminalDoc類的構造函數主要完成一些通信參數的初始化工作。OnNewDocument成員函數創建了三個事件對象,CTerminalDoc的析構函數關閉串行口並刪除事件對象句柄。
OnFileSettings是File->Settings...的命令處理函數,該函數彈出一個CSetupDlg對話框來設置通信參數。實際的設置工作由ConfigConnection函數完成,在OpenConnection和OnFileSettings中都會調用該函數。
OpenConnection負責打開串行口並建立輔助工作線程,當用戶選擇了File->Connect命令時,消息處理函數OnFileConnect將調用該函數。該函數調用CreateFile以重疊方式打開指定的串行口並把返回的句柄保存在m_hCom成員中。接着,函數對m_hCom通信設備進行各種設置。需要注意的是對超時的設定,將讀間隔超時設置爲MAXDWORD並使其它讀超時參數爲0會導致ReadFile函數立即完成操作並返回,而不管讀入了多少字符。設置超時就規定了GetOverlappedResult函數的等待時間,因此有必要將寫超時設置成適當的值,這樣如果不能完成寫串口的任務,GetOverlappedResult函數會在超過規定超時後結束等待並報告實際傳輸的字符數。
如果對m_hCom設置成功,則函數會建立一個輔助線程並暫時將其掛起。在最後,調用CWinThread:: ResumeThread使線程開始運行。
OpenConnection調用成功後,線程函數CommProc就開始工作。該函數的主體是一個while循環,在該循環內,混合了兩種方法監視串行口輸入的方法。先是調用ClearCommError函數查詢輸入緩衝區中是否有字符,如果有,就向視圖發送WM_COMMNOTIFY消息通知其接收字符。如果沒有,則調用WaitCommEvent函數監視EV_RXCHAR通信事件,該函數執行重疊操作,緊接着調用的GetOverlappedResult函數無限等待通信事件,如果EV_RXCHAR事件發生(串口收到字符並放入輸入緩衝區中),那麼函數就結束等待。
上述兩種方法的混合使用兼顧了線程的效率和可靠性。如果只用ClearCommError函數,則輔助線程將不斷耗費CPU時間來查詢,效率較低。如果只用WaitCommEvent來監視,那麼由於該函數對輸入緩衝區中已有的字符不會產生EV_RXCHAR事件,因此在通信速率較高時,會造成數據的延誤和丟失。
注意到輔助線程用m_PostMsgEvent事件對象來同步WM_COMMNOTIFY消息的發送。在發送消息之前,WaitForSingleObject函數無限等待m_PostMsgEvent對象,WM_COMMNOTIFY的消息處理函數CTerminalView::OnCommNotify在返回時會把該對象置爲有信號,因此,如果WaitForSingleObject函數返回,則說明上一個WM_COMMNOTIFY消息已被處理完,這時才能發下一個消息,在發消息前還要調用ResetEvent把m_PostMsgEvent對象置爲無信號的,以供下次使用。
由於PostMessage函數在消息隊列中放入消息後會立即返回,所以如果不採取上述措施,那麼輔助線程可能在主線程未處理之前重複發出WM_COMMNOTIFY消息,這會降低系統的效率。
可能有讀者會問,爲什麼不用SendMessage?該函數在發送的消息被處理完畢後才返回,這樣不就不用考慮同步問題了嗎?是的,本例中也可以使用SendMessage,但該函數會阻塞輔助線程的執行直到消息處理完畢,這會降低效率。如果用PostMessage,那麼在函數立即返回後線程還可以幹別的事情,因此,考慮到效率問題,這裏使用了PostMessage而不是SendMessage。
函數ReadComm和WriteComm分別用來從m_hCom通信設備中讀/寫指定數量的字符。ReadComm函數很簡單,由於對讀超時的特殊設定,ReadFile函數會立即返回並完成操作,並在length變量中報告實際讀入的字符數。此時,沒有必要調用等待函數或GetOverlappedResult。在WriteComm中,調用GerOverlappedResult來等待操作結果,直到超時發生。不管是否超時,該函數在結束等待後都會報告實際的傳輸字符數。
CloseConnection函數的主要任務是終止輔助線程並關閉m_hCom通信設備。爲了終止線程,該函數設置了一系列信號,以結束輔助線程中的等待和循環,然後調用WaitForSingleObject等待線程結束。
清單12.7 CTerminalView類的部分代碼
// TerminalView.h : interface of the CTerminalView class
/////////////////////////////////////////////////////////////////////////////
class CTerminalView : public CEditView
{
. . .
afx_msg LRESULT OnCommNotify(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP()
};
// TerminalView.cpp : implementation of the CTerminalView class
//
BEGIN_MESSAGE_MAP(CTerminalView, CEditView)
. . .
ON_MESSAGE(WM_COMMNOTIFY, OnCommNotify)
END_MESSAGE_MAP()
LRESULT CTerminalView::OnCommNotify(WPARAM wParam, LPARAM lParam)
{
char buf[MAXBLOCK/4];
CString str;
int nLength, nTextLength;
CTerminalDoc* pDoc=GetDocument();
CEdit& edit=GetEditCtrl();
if(!pDoc->m_bConnected ||
(wParam & EV_RXCHAR)!=EV_RXCHAR) // 是否是EV_RXCHAR事件?
{
SetEvent(pDoc->m_hPostMsgEvent); // 允許發送下一個WM_COMMNOTIFY消息
return 0L;
}
nLength=pDoc->ReadComm(buf,100);
if(nLength)
{
nTextLength=edit.GetWindowTextLength();
edit.SetSel(nTextLength,nTextLength); //移動插入光標到正文末尾
for(int i=0;i<nLength;i++)
{
switch(buf[i])
{
case '/r': // 回車
if(!pDoc->m_bNewLine)
break;
case '/n': // 換行
str+="/r/n";
break;
case '/b': // 退格
edit.SetSel(-1, 0);
edit.ReplaceSel(str);
nTextLength=edit.GetWindowTextLength();
edit.SetSel(nTextLength-1,nTextLength);
edit.ReplaceSel(""); //回退一個字符
str="";
break;
case '/a': // 振鈴
MessageBeep((UINT)-1);
break;
default :
str+=buf[i];
}
}
edit.SetSel(-1, 0);
edit.ReplaceSel(str); // 向編輯視圖中插入收到的字符
}
SetEvent(pDoc->m_hPostMsgEvent); // 允許發送下一個WM_COMMNOTIFY消息
return 0L;
}
void CTerminalView::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// TODO: Add your message handler code here and/or call default
CTerminalDoc* pDoc=GetDocument();
char c=(char)nChar;
if(!pDoc->m_bConnected)return;
pDoc->WriteComm(&c, 1);
if(pDoc->m_bEcho)
CEditView::OnChar(nChar, nRepCnt, nFlags); // 本地回顯
}
CTerminalView是CEditView的派生類,利用CEditView的編輯功能,可以大大簡化程序的設計。
OnChar函數對WM_CHAR消息進行處理,它調用CTerminalDoc::WriteComm把用戶鍵入的字符從串行口輸出。如果設置了Local echo,那麼就調用CEditView::OnChar把字符輸出到視圖中。
OnCommNotify是WM_COMMNOTIFY消息的處理函數。該函數調用CTerminalDoc::ReadComm從串行口輸入緩衝區中讀入字符並把它們輸出到編輯視圖中。在輸出前,函數會對一些特殊字符進行處理。如果讀者對控制編輯視圖的代碼不太明白,那麼請參見6.1.4。在函數返回時,要調用SetEvent把m_hPostMsgEvent置爲有信號。
清單12.8 CSetupDlg類的部分代碼
// SetupDlg.h : header file
//
class CSetupDlg : public CDialog
{
. . .
public:
BOOL m_bConnected;
. . .
};
// SetupDlg.cpp : implementation file
//
BOOL CSetupDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// TODO: Add extra initialization here
GetDlgItem(IDC_PORT)->EnableWindow(!m_bConnected);
return TRUE; // return TRUE unless you set the focus to a control
// EXCEPTION: OCX Property Pages should return FALSE
}
CSetupDlg的主要任務是配置通信參數。在OnInitDialog函數中,要根據當前是否連接來允許/禁止Port組合框。因爲在打開一個連接後,顯然不能隨便改變端口。
小 結
本章重點介紹了Win 32環境下的多線程和串行通信編程。本章的要點如下:
Windows 3.x實行的是協同式多任務,應用程序必須“自覺”地放棄CPU控制權,否則系統會被掛起。
Windows 95/NT實現了搶先式多任務,應用程序對CPU的控制時間由系統分配,系統可以在任何時候中斷應用程序,並把控制權轉交給別的程序。
在Win 32環境下,每個進程可以同時執行多個線程。線程是系統分配CPU時間片的基本實體,系統在所有線程之間快速切換以實現多任務。
由於同一進程的所有線程共享進程的虛擬地址空間、Windows 95的重入問題、MFC在對象級的線程不安全性以及線程之間的協調等原因,多個線程必須同步執行。同步機制是由同步對象和等待函數共同實現的。同步對象主要包括事件、mutex和信號燈,進程和線程句柄、文件和通信設備也可以用作同步對象。
在Win 32中,傳統的OpenComm、ReadComm、WriteComm、CloseComm等串行通信函數已經過時,WM_COMMNOTIFY消息也消失了。程序應該調用CreateFile打開一個串行通信設備,用ReadFile和WriteFile來進行I/O操作,用WaitCommEvent來監視通信事件。ReadFile、WriteFile和WaitCommEvent既可以同步操作,也可以重疊操作。
利用Win 32的重疊I/O操作和多線程特性,程序員可以編寫出高效的通信程序。