1 多任務(wù)、進程和線程
Windows是一個多任務(wù)操作系統(tǒng)。傳統(tǒng)的Windows 3.x只能依靠應(yīng)用程序之間的協(xié)同來實現(xiàn)協(xié)同式多任務(wù),而Windows 95/NT實行的是搶先式多任務(wù)。 在Win 32(95/NT)中,每一個進程可以同時執(zhí)行多個線程,這意味著一個程序可以同時完成多個任務(wù)。對于象通信程序這樣既要進行耗時的工作,又要保持對用戶輸入響應(yīng)的應(yīng)用來說,使用多線程是最佳選擇。當(dāng)進程使用多個線程時,需要采取適當(dāng)?shù)拇胧﹣肀3志€程間的同步。 利用Win 32的重疊I/O操作和多線程特性,程序員可以編寫出高效的通信程序。在這一講的最后將通過一個簡單的串行通信程序,向讀者演示多線程和重疊I/O的編程技術(shù)。 1.1 Windows 3.x的協(xié)同多任務(wù) 在16位的Windows 3.x中,應(yīng)用程序具有對CPU的控制權(quán)。只有在調(diào)用了GetMessage、PeekMessage、WaitMessage或Yield后,程序才有可能把CPU控制權(quán)交給系統(tǒng),系統(tǒng)再把控制權(quán)轉(zhuǎn)交給別的應(yīng)用程序。如果應(yīng)用程序在長時間內(nèi)無法調(diào)用上述四個函數(shù)之一,那么程序就一直獨占CPU,系統(tǒng)會被掛起而無法接受用戶的輸入。 因此,在設(shè)計16位的應(yīng)用程序時,程序員必須合理地設(shè)計消息處理函數(shù),以使程序能夠盡快返回到消息循環(huán)中。如果程序需要進行費時的操作,那么必須保證程序在進行操作時能周期性的調(diào)用上述四個函數(shù)中的一個。 在Windows 3.x環(huán)境下,要想設(shè)計一個既能執(zhí)行實時的后臺工作(如對通信端口的實時監(jiān)測和讀寫),又能保證所有界面響應(yīng)用戶輸入的單獨的應(yīng)用程序幾乎是不可能的。 有人可能會想到用CWinApp::OnIdle函數(shù)來執(zhí)行后臺工作,因為該函數(shù)是程序主消息循環(huán)在空閑時調(diào)用的。但OnIdle的執(zhí)行并不可靠,例如,如果用戶在程序中打開了一個菜單或模態(tài)對話框,那么OnIdle將停止調(diào)用,因為此時程序不能返回到主消息循環(huán)中!在實時任務(wù)代碼中調(diào)用 PeekMessage也會遇到同樣的問題,除非程序能保證用戶不會選擇菜單或彈出模態(tài)對話框,否則程序?qū)⒉荒芊祷氐絇eekMessage的調(diào)用處,這將導(dǎo)致后臺實時處理的中斷。 折衷的辦法是在執(zhí)行長期工作時彈出一個非模態(tài)對話框并禁止主窗口,在消息循環(huán)內(nèi)分批執(zhí)行后臺操作。對話框中可以顯示工作的進度,也可以包含一個取消按鈕以讓用戶有機會中斷一個長期的工作。典型的代碼如清單12.1所示。這樣做既可以保證工作實時進行,又可以使程序能有限地響應(yīng)用戶輸入,但此時程序?qū)嶋H上已不能再為用戶干別的事情了。 //清單12.1 在協(xié)同多任務(wù)環(huán)境下防止程序被掛起的一種方法
bAbort=FALSE; lpMyDlgProc=MakeProcInstance(MyDlgProc, hInst); hMyDlg=CreateDialog(hInst, “Abort”, hwnd, lpMyDlgProc); //創(chuàng)建一個非模態(tài)對話框 ShowWindow(hMyDlg, SW_NORMAL); UpdateWindow(hMyDlg); EnableWindow(hwnd, FALSE); //禁止主窗口 while(!bAbort) { //執(zhí)行一次后臺操作 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的搶先式多任務(wù) 線程分用戶界面線程和工作者線程兩種。用戶界面線程擁有自己的消息泵來處理界面消息,可以與用戶進行交互。工作者線程沒有消息泵,一般用來完成后臺工作。 MFC應(yīng)用程序的線程由對象CWinThread表示。在多數(shù)情況下,程序不需要自己創(chuàng)建CWinThread對象。調(diào)用AfxBeginThread函數(shù)時會自動創(chuàng)建一個CWinThread對象。 例如,清單12.2中的代碼演示了工作者線程的創(chuàng)建。AfxBeginThread函數(shù)負責(zé)創(chuàng)建新線程,它的第一個參數(shù)是代表線程的函數(shù)的地址,在本例中是MyThreadProc。第二個參數(shù)是傳遞給線程函數(shù)的參數(shù),這里假定線程要用到CMyObject對象,所以把pNewObject指針傳給了新線程。線程函數(shù)MyThreadProc用來執(zhí)行線程,請注意該函數(shù)的聲明。線程函數(shù)有一個32位的pParam參數(shù)可用來接收必要的參數(shù)。 清單12.2 創(chuàng)建一個工作者線程
//主線程 pNewObject = new CMyObject; AfxBeginThread(MyThreadProc, pNewObject); //新線程 UINT MyThreadProc( LPVOID pParam ) { CMyObject* pObject = (CMyObject*)pParam; if (pObject == NULL || !pObject->IsKindOf(RUNTIME_CLASS(CMyObject))) return -1; // 非法參數(shù) // 用pObject對象來完成某項工作 return 0; // 線程正常結(jié)束 }
AfxBeginThread的聲明為: 參數(shù)pfnThreadProc是工作線程函數(shù)的地址。pParam是傳遞給線程函數(shù)的參數(shù)。nPriority 是線程的優(yōu)先級,一般是THREAD_PRIORITY_NORMAL,若為0,則使用創(chuàng)建線程的優(yōu)先級。nStackSize說明了線程的堆棧尺寸,若為0則堆棧尺寸與創(chuàng)建線程相同。dwCreateFlags指定了線程的初始狀態(tài),如果為0,那么線程在創(chuàng)建后立即執(zhí)行,如果為 CREATE_SUSPENDED,則線程在創(chuàng)建后就被掛起。參數(shù)lpSecurityAttrs用來說明保密屬性,一般為0。函數(shù)返回新建的 CWinThread對象的指針。 程序應(yīng)該把AfxBeginThread 返回的CWinThread指針保存起來,以便對創(chuàng)建的線程進行控制。例如,可以調(diào)用CWinThread::SetThreadPriority來設(shè)置線程的優(yōu)先級,用CWinThread::SuspendThread來掛起線程。如果線程被掛起,那么直到調(diào)用CWinThread:: ResumeThread后線程才開始運行。 如果要創(chuàng)建用戶界面線程,那么必須從CWinThread派生一個新類。事實上,代表進程主線程的CWinApp類就是CWinThread的派生類。派生類必須用 DECLARE_DYNCREATE和IMPLEMENT_DYNCREATE宏來聲明和實現(xiàn)。需要重寫派生類的InitInstance、 ExitInstance、Run等函數(shù)。 可以使用AfxBeginThread函數(shù)的另一個版本來創(chuàng)建用戶界面線程。函數(shù)的聲明為:
2 線程的同步 多線程的使用會產(chǎn)生一些新的問題,主要是如何保證線程的同步執(zhí)行。多線程應(yīng)用程序需要使用同步對象和等待函數(shù)來實現(xiàn)同步。 12.2.1 為什么需要同步 由于同一進程的所有線程共享進程的虛擬地址空間,并且線程的中斷是匯編語言級的,所以可能會發(fā)生兩個線程同時訪問同一個對象(包括全局變量、共享資源、 API函數(shù)和MFC對象等)的情況,這有可能導(dǎo)致程序錯誤。例如,如果一個線程在未完成對某一大尺寸全局變量的讀操作時,另一個線程又對該變量進行了寫操作,那么第一個線程讀入的變量值可能是一種修改過程中的不穩(wěn)定值。 屬于不同進程的線程在同時訪問同一內(nèi)存區(qū)域或共享資源時,也會存在同樣的問題。 因此,在多線程應(yīng)用程序中,常常需要采取一些措施來同步線程的執(zhí)行。需要同步的情況包括以下幾種:
12.2.2 等待函數(shù) Win32 API提供了一組能使線程阻塞其自身執(zhí)行的等待函數(shù)。這些函數(shù)只有在作為其參數(shù)的一個或多個同步對象(見下小節(jié))產(chǎn)生信號時才會返回。在超過規(guī)定的等待時間后,不管有無信號,函數(shù)也都會返回。在等待函數(shù)未返回時,線程處于等待狀態(tài),此時線程只消耗很少的CPU時間。 使用等待函數(shù)即可以保證線程的同步,又可以提高程序的運行效率。最常用的等待函數(shù)是WaitForSingleObject,該函數(shù)的聲明為:
參數(shù)hHandle是同步對象的句柄。參數(shù)dwMilliseconds是以毫秒為單位的超時間隔,如果該參數(shù)為0,那么函數(shù)就測試同步對象的狀態(tài)并立即返回,如果該參數(shù)為INFINITE,則超時間隔是無限的。函數(shù)的返回值在表12.1中列出。
表12.1 WaitForSingleObject的返回值
函數(shù)WaitForMultipleObjects可以同時監(jiān)測多個同步對象,該函數(shù)的聲明為:
參數(shù)nCount是句柄數(shù)組中句柄的數(shù)目。lpHandles代表一個句柄數(shù)組。bWaitAll說明了等待類型,如果為TRUE,那么函數(shù)在所有對象都有信號后才返回,如果為FALSE,則只要有一個對象變成有信號的,函數(shù)就返回。函數(shù)的返回值在表12.2中列出。參數(shù)dwMilliseconds是以毫秒為單位的超時間隔,如果該參數(shù)為0,那么函數(shù)就測試同步對象的狀態(tài)并立即返回,如果該參數(shù)為INFINITE,則超時間隔是無限的。
表12.2 WaitForMultipleObjects的返回值
12.2.3 同步對象 同步對象用來協(xié)調(diào)多線程的執(zhí)行,它可以被多個線程共享。線程的等待函數(shù)用同步對象的句柄作為參數(shù),同步對象應(yīng)該是所有要使用的線程都能訪問到的。同步對象的狀態(tài)要么是有信號的,要么是無信號的。同步對象主要有三種:事件、mutex和信號燈。 事件對象(Event)是最簡單的同步對象,它包括有信號和無信號兩種狀態(tài)。在線程訪問某一資源之前,也許需要等待某一事件的發(fā)生,這時用事件對象最合適。例如,只有在通信端口緩沖區(qū)收到數(shù)據(jù)后,監(jiān)視線程才被激活。 事件對象是用CreateEvent函數(shù)建立的。該函數(shù)可以指定事件對象的種類和事件的初始狀態(tài)。如果是手工重置事件,那么它總是保持有信號狀態(tài),直到用ResetEvent函數(shù)重置成無信號的事件。如果是自動重置事件,那么它的狀態(tài)在單個等待線程釋放后會自動變?yōu)闊o信號的。用SetEvent可以把事件對象設(shè)置成有信號狀態(tài)。在建立事件時,可以為對象起個名字,這樣其它進程中的線程可以用OpenEvent函數(shù)打開指定名字的事件對象句柄。 mutex對象的狀態(tài)在它不被任何線程擁有時是有信號的,而當(dāng)它被擁有時則是無信號的。mutex對象很適合用來協(xié)調(diào)多個線程對共享資源的互斥訪問(mutually exclusive)。 線程用CreateMutex函數(shù)來建立mutex對象,在建立mutex時,可以為對象起個名字,這樣其它進程中的線程可以用OpenMutex函數(shù)打開指定名字的mutex對象句柄。在完成對共享資源的訪問后,線程可以調(diào)用ReleaseMutex來釋放mutex,以便讓別的線程能訪問共享資源。如果線程終止而不釋放mutex,則認(rèn)為該mutex被廢棄。 信號燈對象維護一個從0開始的計數(shù),在計數(shù)值大于0時對象是有信號的,而在計數(shù)值為0時則是無信號的。信號燈對象可用來限制對共享資源進行訪問的線程數(shù)量。線程用 CreateSemaphore函數(shù)來建立信號燈對象,在調(diào)用該函數(shù)時,可以指定對象的初始計數(shù)和最大計數(shù)。在建立信號燈時也可以為對象起個名字,別的進程中的線程可以用OpenSemaphore函數(shù)打開指定名字的信號燈句柄。 一般把信號燈的初始計數(shù)設(shè)置成最大值。每次當(dāng)信號燈有信號使等待函數(shù)返回時,信號燈計數(shù)就會減1,而調(diào)用ReleaseSemaphore可以增加信號燈的計數(shù)。計數(shù)值越小就表明訪問共享資源的程序越多。 除了上述三種同步對象外,表12.3中的對象也可用于同步。另外,有時可以用文件或通信設(shè)備作為同步對象使用。
表12.3 可用于同步的對象
當(dāng)對象不再使用時,應(yīng)該用CloseHandle函數(shù)關(guān)閉對象句柄。 清單12.3是一個使用事件對象的簡單例子,在該例中,假設(shè)主線程要讀取共享緩沖區(qū)中的內(nèi)容,而輔助線程負責(zé)向緩沖區(qū)中寫入數(shù)據(jù)。兩個線程使用了一個 hEvent事件對象來同步。在用CreateEvent函數(shù)創(chuàng)建事件對象句柄時,指定該對象是一個自動重置事件,其初始狀態(tài)為有信號的。當(dāng)線程要讀寫緩沖區(qū)時,調(diào)用WaitForSingleObject函數(shù)無限等待hEvent信號。如果hEvent無信號,則說明另一線程正在訪問緩沖區(qū);如果有信號,則本線程可以訪問緩沖區(qū),WaitForSingleObject函數(shù)在返回后會自動把hEvent置成無信號的,這樣在本線程讀寫緩沖區(qū)時別的線程不會同時訪問。在完成讀寫操作后,調(diào)用SetEvent函數(shù)把hEvent置成有信號的,以使別的線程有機會訪問共享緩沖區(qū)。
清單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; // 線程正常結(jié)束 } 12.2.4 關(guān)鍵節(jié)和互鎖變量訪問 關(guān)鍵節(jié)(Critical Seciton)與mutex的功能類似,但它只能由同一進程中的線程使用。關(guān)鍵節(jié)可以防止共享資源被同時訪問。 進程負責(zé)為關(guān)鍵節(jié)分配內(nèi)存空間,關(guān)鍵節(jié)實際上是一個CRITICAL_SECTION型的變量,它一次只能被一個線程擁有。在線程使用關(guān)鍵節(jié)之前,必須調(diào)用InitializeCriticalSection函數(shù)將其初始化。如果線程中有一段關(guān)鍵的代碼不希望被別的線程中斷,那么可以調(diào)用 EnterCriticalSection函數(shù)來申請關(guān)鍵節(jié)的所有權(quán),在運行完關(guān)鍵代碼后再用LeaveCriticalSection函數(shù)來釋放所有權(quán)。如果在調(diào)用EnterCriticalSection時關(guān)鍵節(jié)對象已被另一個線程擁有,那么該函數(shù)將無限期等待所有權(quán)。 利用互鎖變量可以建立簡單有效的同步機制。使用函數(shù)InterlockedIncrement和InterlockedDecrement可以增加或減少多個線程共享的一個32位變量的值,并且可以檢查結(jié)果是否為0。線程不必擔(dān)心會被其它線程中斷而導(dǎo)致錯誤。如果變量位于共享內(nèi)存中,那么不同進程中的線程也可以使用這種機制。 3 串行通信與重疊I/O Win 32系統(tǒng)為串行通信提供了全新的服務(wù)。傳統(tǒng)的OpenComm、ReadComm、WriteComm、CloseComm等函數(shù)已經(jīng)過時,WM_COMMNOTIFY消息也消失了。取而代之的是文件I/O函數(shù)提供的打開和關(guān)閉通信資源句柄及讀寫操作的基本接口。 新的文件I/O函數(shù)(CreateFile、ReadFile、WriteFile等)支持重疊式輸入輸出,這使得線程可以從費時的I/O操作中解放出來,從而極大地提高了程序的運行效率。 12.3.1 串行口的打開和關(guān)閉 Win 32系統(tǒng)把文件的概念進行了擴展。無論是文件、通信設(shè)備、命名管道、郵件槽、磁盤、還是控制臺,都是用API函數(shù)CreateFile來打開或創(chuàng)建的。該函數(shù)的聲明為: HANDLE CreateFile( LPCTSTR lpFileName, // 文件名 DWORD dwDesiredAccess, // 訪問模式 DWORD dwShareMode, // 共享模式 LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 通常為NULL DWORD dwCreationDistribution, // 創(chuàng)建方式 DWORD dwFlagsAndAttributes, // 文件屬性和標(biāo)志 HANDLE hTemplateFile // 臨時文件的句柄,通常為NULL
如果調(diào)用成功,那么該函數(shù)返回文件的句柄,如果調(diào)用失敗,則函數(shù)返回INVALID_HANDLE_VALUE。 如果想要用重疊I/O方式(參見12.3.3)打開COM2口,則一般應(yīng)象清單12.4那樣調(diào)用CreateFile函數(shù)。注意在打開一個通信端口時,應(yīng)該以獨占方式打開,另外要指定GENERIC_READ、GENERIC_WRITE、OPEN_EXISTING和 FILE_ATTRIBUTE_NORMAL等屬性。如果要打開重疊I/O,則應(yīng)該指定 FILE_FLAG_OVERLAPPED屬性。
清單12.4
HANDLE hCom; DWORD dwError; hCom=CreateFile(“COM2”, //文件名 GENERIC_READ | GENERIC_WRITE, // 允許讀和寫 0, // 獨占方式 NULL, OPEN_EXISTING, //打開而不是創(chuàng)建 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // 重疊方式 NULL ); if(hCom = = INVALID_HANDLE_VALUE) { dwError=GetLastError( ); . . . // 處理錯誤 } 當(dāng)不再使用文件句柄時,應(yīng)該調(diào)用CloseHandle函數(shù)關(guān)閉之。 12.3.2 串行口的初始化 在打開通信設(shè)備句柄后,常常需要對串行口進行一些初始化工作。這需要通過一個DCB結(jié)構(gòu)來進行。DCB結(jié)構(gòu)包含了諸如波特率、每個字符的數(shù)據(jù)位數(shù)、奇偶校驗和停止位數(shù)等信息。在查詢或配置置串行口的屬性時,都要用DCB結(jié)構(gòu)來作為緩沖區(qū)。 調(diào)用GetCommState函數(shù)可以獲得串口的配置,該函數(shù)把當(dāng)前配置填充到一個DCB結(jié)構(gòu)中。一般在用CreateFile打開串行口后,可以調(diào)用 GetCommState函數(shù)來獲取串行口的初始配置。要修改串行口的配置,應(yīng)該先修改DCB結(jié)構(gòu),然后再調(diào)用SetCommState函數(shù)用指定的 DCB結(jié)構(gòu)來設(shè)置串行口。 除了在DCB中的設(shè)置外,程序一般還需要設(shè)置I/O緩沖區(qū)的大小和超時。Windows用I/O緩沖區(qū)來暫存串行口輸入和輸出的數(shù)據(jù),如果通信的速率較高,則應(yīng)該設(shè)置較大的緩沖區(qū)。調(diào)用SetupComm函數(shù)可以設(shè)置串行口的輸入和輸出緩沖區(qū)的大小。 在用ReadFile和WriteFile讀寫串行口時,需要考慮超時問題。如果在指定的時間內(nèi)沒有讀出或?qū)懭胫付〝?shù)量的字符,那么ReadFile或 WriteFile的操作就會結(jié)束。要查詢當(dāng)前的超時設(shè)置應(yīng)調(diào)用GetCommTimeouts函數(shù),該函數(shù)會填充一個COMMTIMEOUTS結(jié)構(gòu)。調(diào)用SetCommTimeouts可以用某一個COMMTIMEOUTS結(jié)構(gòu)的內(nèi)容來設(shè)置超時。 有兩種超時:間隔超時和總超時。間隔超時是指在接收時兩個字符之間的最大時延,總超時是指讀寫操作總共花費的最大時間。寫操作只支持總超時,而讀操作兩種超時均支持。用COMMTIMEOUTS結(jié)構(gòu)可以規(guī)定讀/寫操作的超時,該結(jié)構(gòu)的定義為:
COMMTIMEOUTS結(jié)構(gòu)的成員都以毫秒為單位。總超時的計算公式是: 總超時=時間系數(shù)×要求讀/寫的字符數(shù) + 時間常量 例如,如果要讀入10個字符,那么讀操作的總超時的計算公式為: 讀總超時=ReadTotalTimeoutMultiplier×10 + ReadTotalTimeoutConstant 可以看出,間隔超時和總超時的設(shè)置是不相關(guān)的,這可以方便通信程序靈活地設(shè)置各種超時。 如果所有寫超時參數(shù)均為0,那么就不使用寫超時。如果ReadIntervalTimeout為0,那么就不使用讀間隔超時,如果 ReadTotalTimeoutMultiplier和ReadTotalTimeoutConstant都為0,則不使用讀總超時。如果讀間隔超時被設(shè)置成MAXDWORD并且兩個讀總超時為0,那么在讀一次輸入緩沖區(qū)中的內(nèi)容后讀操作就立即完成,而不管是否讀入了要求的字符。 在用重疊方式讀寫串行口時,雖然ReadFile和WriteFile在完成操作以前就可能返回,但超時仍然是起作用的。在這種情況下,超時規(guī)定的是操作的完成時間,而不是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, //打開而不是創(chuàng)建 FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, // 重疊方式 NULL ); if(hCom = = INVALID_HANDLE_VALUE) { dwError=GetLastError( ); . . . // 處理錯誤 } SetupComm( hCom, 1024, 1024 ) //緩沖區(qū)的大小為1024 TimeOuts. ReadIntervalTimeout=1000; TimeOuts.ReadTotalTimeoutMultiplier=500; TimeOuts.ReadTotalTimeoutConstant=5000; TimeOuts.WriteTotalTimeoutMultiplier=500; TimeOuts.WriteTotalTimeoutConstant=5000; SetCommTimeouts(hCom, &TimeOuts); // 設(shè)置超時 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讀寫串行口時,既可以同步執(zhí)行,也可以重疊(異步)執(zhí)行。在同步執(zhí)行時,函數(shù)直到操作完成后才返回。這意味著在同步執(zhí)行時線程會被阻塞,從而導(dǎo)致效率下降。在重疊執(zhí)行時,即使操作還未完成,調(diào)用的函數(shù)也會立即返回。費時的I/O操作在后臺進行,這樣線程就可以干別的事情。例如,線程可以在不同的句柄上同時執(zhí)行I/O操作,甚至可以在同一句柄上同時進行讀寫操作。“重疊”一詞的含義就在于此。 ReadFile函數(shù)只要在串行口輸入緩沖區(qū)中讀入指定數(shù)量的字符,就算完成操作。而WriteFile函數(shù)不但要把指定數(shù)量的字符拷入到輸出緩沖中,而且要等這些字符從串行口送出去后才算完成操作。 ReadFile和WriteFile函數(shù)是否為執(zhí)行重疊操作是由CreateFile函數(shù)決定的。如果在調(diào)用CreateFile創(chuàng)建句柄時指定了 FILE_FLAG_OVERLAPPED標(biāo)志,那么調(diào)用ReadFile和WriteFile對該句柄進行的讀寫操作就是重疊的,如果未指定重疊標(biāo)志,則讀寫操作是同步的。 函數(shù)ReadFile和WriteFile的參數(shù)和返回值很相似。這里僅列出ReadFile函數(shù)的聲明: BOOL ReadFile(
需要注意的是如果該函數(shù)因為超時而返回,那么返回值是TRUE。參數(shù)lpOverlapped在重疊操作時應(yīng)該指向一個OVERLAPPED結(jié)構(gòu),如果該參數(shù)為NULL,那么函數(shù)將進行同步操作,而不管句柄是否是由FILE_FLAG_OVERLAPPED標(biāo)志建立的。 當(dāng)ReadFile和WriteFile返回FALSE時,不一定就是操作失敗,線程應(yīng)該調(diào)用GetLastError函數(shù)分析返回的結(jié)果。例如,在重疊操作時如果操作還未完成函數(shù)就返回,那么函數(shù)就返回FALSE,而且GetLastError函數(shù)返回ERROR_IO_PENDING。 在使用重疊I/O時,線程需要創(chuàng)建OVERLAPPED結(jié)構(gòu)以供讀寫函數(shù)使用。OVERLAPPED結(jié)構(gòu)最重要的成員是hEvent,hEvent是一個事件對象句柄,線程應(yīng)該用CreateEvent函數(shù)為hEvent成員創(chuàng)建一個手工重置事件,hEvent成員將作為線程的同步對象使用。如果讀寫函數(shù)未完成操作就返回,就那么把hEvent成員設(shè)置成無信號的。操作完成后(包括超時),hEvent會變成有信號的。 如果GetLastError函數(shù)返回ERROR_IO_PENDING,則說明重疊操作還為完成,線程可以等待操作完成。有兩種等待辦法:一種辦法是用象WaitForSingleObject這樣的等待函數(shù)來等待OVERLAPPED結(jié)構(gòu)的hEvent成員,可以規(guī)定等待的時間,在等待函數(shù)返回后,調(diào)用GetOverlappedResult。另一種辦法是調(diào)用GetOverlappedResult函數(shù)等待,如果指定該函數(shù)的bWait參數(shù)為 TRUE,那么該函數(shù)將等待OVERLAPPED結(jié)構(gòu)的hEvent 事件。GetOverlappedResult可以返回一個OVERLAPPED結(jié)構(gòu)來報告包括實際傳輸字節(jié)在內(nèi)的重疊操作結(jié)果。 如果規(guī)定了讀/寫操作的超時,那么當(dāng)超過規(guī)定時間后,hEvent成員會變成有信號的。因此,在超時發(fā)生后,WaitForSingleObject和 GetOverlappedResult都會結(jié)束等待。WaitForSingleObject的dwMilliseconds參數(shù)會規(guī)定一個等待超時,該函數(shù)實際等待的時間是兩個超時的最小值。注意GetOverlappedResult不能設(shè)置等待的時限,因此如果hEvent成員無信號,則該函數(shù)將一直等待下去。 在調(diào)用ReadFile和WriteFile之前,線程應(yīng)該調(diào)用ClearCommError函數(shù)清除錯誤標(biāo)志。該函數(shù)負責(zé)報告指定的錯誤和設(shè)備的當(dāng)前狀態(tài)。 調(diào)用PurgeComm函數(shù)可以終止正在進行的讀寫操作,該函數(shù)還會清除輸入或輸出緩沖區(qū)中的內(nèi)容。
12.3.4 通信事件 在Windows 95/NT中,WM_COMMNOTIFY消息已經(jīng)取消,在串行口產(chǎn)生一個通信事件時,程序并不會收到通知消息。線程需要調(diào)用WaitCommEvent 函數(shù)來監(jiān)視發(fā)生在串行口中的各種事件,該函數(shù)的第二個參數(shù)返回一個事件屏蔽變量,用來指示事件的類型。線程可以用SetCommMask建立事件屏蔽以指定要監(jiān)視的事件,表12.4列出了可以監(jiān)視的事件。調(diào)用GetCommMask可以查詢串行口當(dāng)前的事件屏蔽。
表12.4 通信事件
WaitCommEvent即可以同步使用,也可以重疊使用。如果串口是用FILE_FLAG_OVERLAPPED標(biāo)志打開的,那么 WaitCommEvent就進行重疊操作,此時該函數(shù)需要一個OVERLAPPED結(jié)構(gòu)。線程可以調(diào)用等待函數(shù)或 GetOverlappedResult函數(shù)來等待重疊操作的完成。 當(dāng)指定范圍內(nèi)的某一事件發(fā)生后,線程就結(jié)束等待并把該事件的屏蔽碼設(shè)置到事件屏蔽變量中。需要注意的是,WaitCommEvent只檢測調(diào)用該函數(shù)后發(fā)生的事件。例如,如果在調(diào)用WaitCommEvent前在輸入緩沖區(qū)中就有字符,則不會因為這些字符而產(chǎn)生EV_RXCHAR事件。 如果檢測到輸入的硬件信號(如CTS、RTS和CD信號等)發(fā)生了變化,線程可以調(diào)用GetCommMaskStatus函數(shù)來查詢它們的狀態(tài)。而用EscapeCommFunction函數(shù)可以控制輸出的硬件信號(如DTR和RTS信號)。 |
|