多線程與串行通信
1 多任務、進程和線程Windows是一個多任務操作系統。傳統的Windows 3.x只能依靠應用程序之間的協同來實現協同式多任務,而Windows 95/NT實行的是搶先式多任務。
在Win 32(95/NT)中,每一個進程可以同時執行多個線程,這意味著一個程序可以同時完成多個任務。對于象通信程序這樣既要進行耗時的工作,又要保持對用戶 輸入響應的應用來說,使用多線程是最佳選擇。當進程使用多個線程時,需要采取適當的措施來保持線程間的同步。
利用Win 32的重疊I/O操作和多線程特性,程序員可以編寫出高效的通信程序。在這一講的最后將通過一個簡單的串行通信程序,向讀者演示多線程和重疊I/O的編程技術。
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所示。這樣做既可以保證工作實時進行,又可以使程序能有限地響應用戶輸入,但此時程序實際上已不能再為用戶干別的事情了。
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);
1.2 Windows 95/NT的搶先式多任務
在32位的Windows系統中,采用的是搶先式多任務,這意味著程序對CPU的占用時間是由系統決定的。系統為每個程序分配一定的CPU時間,當程序的運行超過規定時間后,系統就會中斷該程序并把CPU控制權轉交給別的程序。與協同式多任務不同,這種中斷是匯編語言級的。程序不必調用象 PeekMessage這樣的函數來放棄對CPU的控制權,就可以進行費時的工作,而且不會導致系統的掛起。
例如,在Windows3.x 中,如果某一個應用程序陷入了死循環,那么整個系統都會癱瘓,這時唯一的解決辦法就是重新啟動機器。而在Windows 95/NT中,一個程序的崩潰一般不會造成死機,其它程序仍然可以運行,用戶可以按Ctrl+Alt+Del鍵來打開任務列表并關閉沒有響應的程序。
1.3 進程與線程
在32位的Windows系統中,術語多任務是指系統可以同時運行多個進程,而每個進程也可以同時執行多個線程。
進程就是應用程序的運行實例。每個進程都有自己私有的虛擬地址空間。每個進程都有一個主線程,但可以建立另外的線程。進程中的線程是并行執行的,每個線程占用CPU的時間由系統來劃分。
可以把線程看成是操作系統分配CPU時間的基本實體。系統不停地在各個線程之間切換,它對線程的中斷是匯編語言級的。系統為每一個線程分配一個CPU時間片,某個線程只有在分配的時間片內才有對CPU的控制權。實際上,在PC機中,同一時間只有一個線程在運行。由于系統為每個線程劃分的時間片很小(20 毫秒左右),所以看上去好象是多個線程在同時運行。
進程中的所有線程共享進程的虛擬地址空間,這意味著所有線程都可以訪問進程的全局變量和資源。這一方面為編程帶來了方便,但另一方面也容易造成沖突。
雖然在進程中進行費時的工作不會導致系統的掛起,但這會導致進程本身的掛起。所以,如果進程既要進行長期的工作,又要響應用戶的輸入,那么它可以啟動一個線程來專門負責費時的工作,而主線程仍然可以與用戶進行交互。
1.4
線程的創建和終止
線程分用戶界面線程和工作者線程兩種。用戶界面線程擁有自己的消息泵來處理界面消息,可以與用戶進行交互。工作者線程沒有消息泵,一般用來完成后臺工作。
MFC應用程序的線程由對象CWinThread表示。在多數情況下,程序不需要自己創建CWinThread對象。調用AfxBeginThread函數時會自動創建一個CWinThread對象。
例如,清單12.2中的代碼演示了工作者線程的創建。AfxBeginThread函數負責創建新線程,它的第一個參數是代表線程的函數的地址,在本例 中是MyThreadProc。第二個參數是傳遞給線程函數的參數,這里假定線程要用到CMyObject對象,所以把pNewObject指針傳給了新 線程。線程函數MyThreadProc用來執行線程,請注意該函數的聲明。線程函數有一個32位的pParam參數可用來接收必要的參數。
//主線程
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。
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置成有信號的,以使別的線程有機會訪問共享緩沖區。
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。線程不必擔心會被其它線程中斷而導致錯誤。如果變量位于共享內存中,那么不同進程中的線 程也可以使用這種機制。
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屬性。
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列出了一段簡單的串行口初始化代碼。
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信號)。
posted on 2008-01-07 23:40 isabc 閱讀(2605) 評論(1) 編輯 收藏 引用 所屬分類: Win32 多線程