多线程与串行通信_第1页
多线程与串行通信_第2页
多线程与串行通信_第3页
多线程与串行通信_第4页
多线程与串行通信_第5页
已阅读5页,还剩35页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

1、多线程与串行通信Windows是一个多任务操作系统。传统的 Windows 3.x只能依靠应用程序之间的 协同来实现协同式多任务,而 Windows 95/NT实行的是抢先式多任务。在Win 32(95/NT)中,每一个进程可以同时执行多个线程,这意味着一个程 序可以同时完成多个任务。对于象通信程序这样既要进行耗时的工作,又要保持 对用户输入响应的应用来说,使用多线程是最佳选择。当进程使用多个线程时, 需要采取适当的措施来保持线程间的同步。利用Win 32的重叠I/O操作和多线程特性,程序员可以编写出高效的通信程 序。在这一讲的最后将通过一个简单的串行通信程序,向读者演示多线程和重叠 I/O的

2、编程技术。多任务、进程和线程线程的同步串行通信与重叠I/O一个通信演示程序小结12.1.1 Windows 3.x的协同多任务在16位的Windows 3.x中,应用程序具有对 CPU勺控制权。只有在调用了 GetMessage PeekMessage WaitMessage 或 Yield 后,程序才有可能把 CPL控制9 权交给系统,系统再把控制权转交给别的应用程序。如果应用程序在长时间内无 法调用上述四个函数之一,那么程序就一直独占CPU系统会被挂起而无法接受用户的输入。因此,在设计16位的应用程序时,程序员必须合理地设计消息处理函数,以使程序能够尽快返回到消息循环中。如果程序需要进行费

3、时的操作,那么必须保 证程序在进行操作时能周期性的调用上述四个函数中的一个。在Windows 3.x环境下,要想设计一个既能执行实时的后台工作(如对通信 端口的实时监测和读写),又能保证所有界面响应用户输入的单独的应用程序几 乎是不可能的。有人可能会想到用CWinApp:Onldle函数来执行后台工作,因为该函数是程 序主消息循环在空闲时调用的。但OnIdle的执行并不可靠,例如,如果用户在程 序中打开了一个菜单或模态对话框,那么OnIdle将停止调用,因为此时程序不能 返回到主消息循环中!在实时任务代码中调用 PeekMessage也会遇到同样的问题,除非程序能保证用户不会选择菜单或弹出模态

4、对话框,否则程序将不能返回到PeekMessage的调用处,这将导致后台实时处理的中断。折衷的办法是在执行长期工作时弹出一个非模态对话框并禁止主窗口,在消 息循环内分批执行后台操作。对话框中可以显示工作的进度,也可以包含一个取 消按钮以让用户有机会中断一个长期的工作。 典型的代码如清单 12.1 所示。这样 做既可以保证工作实时进行,又可以使程序能有限地响应用户输入,但此时程序 实际上已不能再为用户干别的事情了。清单 12.1 在协同多任务环境下防止程序被挂起的一种方法bAbort=FALSE;lpMyDlgProc=MakeProcInstance(MyDlgProc, hInst);hMy

5、Dlg=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);DispatchMess

6、age(&msg);EnableWindow(hwnd, TRUE); / 允许主窗口DestroyWindow(hMyDlg);FreeProcInstance(lpMyDlgProc);12.1.2 Windows 95/NT 的抢先式多任务在 32 位的 Windows 系统中,采用的是抢先式多任务,这意味着程序对 CPU 的占用时间是由系统决定的。系统为每个程序分配一定的CPU寸间,当程序的运行超过规定时间后,系统就会中断该程序并把CPU空制权转交给别的程序。与协同式多任务不同,这种中断是汇编语言级的。程序不必调用象PeekMessage这样的函数来放弃对CPU的控制权,就可以进行费时

7、的工作,而且不会导致系统的挂 起。例如,在 Windows3.x 中,如果某一个应用程序陷入了死循环,那么整个系 统都会瘫痪,这时唯一的解决办法就是重新启动机器。而在Windows 95/NT中,一个程序的崩溃一般不会造成死机,其它程序仍然可以运行,用户可以按 Ctrl+Alt+Del 键来打开任务列表并关闭没有响应的程序。12.1.3 进程与线程在32位的Windows系统中,术语多任务是指系统可以同时运行多个进程,而每个进程也可以同时执行多个线程。进程就是应用程序的运行实例。每个进程都有自己私有的虚拟地址空间。每 个进程都有一个主线程,但可以建立另外的线程。进程中的线程是并行执行的, 每个

8、线程占用CPU的时间由系统来划分。可以把线程看成是操作系统分配 CPU寸间的基本实体。系统不停地在各个线 程之间切换,它对线程的中断是汇编语言级的。系统为每一个线程分配一个 CPU 时间片,某个线程只有在分配的时间片内才有对CPU勺控制权。实际上,在PC机中,同一时间只有一个线程在运行。 由于系统为每个线程划分的时间片很小 (20 毫秒左右),所以看上去好象是多个线程在同时运行。进程中的所有线程共享进程的虚拟地址空间,这意味着所有线程都可以访问 进程的全局变量和资源。这一方面为编程带来了方便,但另一方面也容易造成冲 突。虽然在进程中进行费时的工作不会导致系统的挂起,但这会导致进程本身的挂起。所

9、以,如果进程既要进行长期的工作,又要响应用户的输入,那么它可以 启动一个线程来专门负责费时的工作,而主线程仍然可以与用户进行交互。12.1.4 线程的创建和终止线程分用户界面线程和工作者线程两种。用户界面线程拥有自己的消息泵来 处理界面消息,可以与用户进行交互。工作者线程没有消息泵,一般用来完成后 台工作。MFC应用程序的线程由对象CWinThread表示。在多数情况下,程序不需要自 己创建CWinThread对象。调用AfxBeginThread函数时会自动创建一个 CWin Thread 对象。例如,清单 12.2 中的代码演示了工作者线程的创建。 AfxBeginThread 函数 负责

10、创建新线程,它的第一个参数是代表线程的函数的地址,在本例中是 MyThreadProc。第二个参数是传递给线程函数的参数,这里假定线程要用到 CMyObject对象,所以把pNewObject指针传给了新线程。线程函数MyThreadProc 用来执行线程,请注意该函数的声明。线程函数有一个32位的pParam参数可用来接收必要的参数。清单 12.2 创建一个工作者线程/ 主线程pNewObject = new CMyObject;AfxBeginThread(MyThreadProc, pNewObject);/ 新线程UINT MyThreadProc( LPVOID pParam )CM

11、yObject* pObject = (CMyObject*)pParam;if (pObject = NULL | !pObject-IsKindOf(RUNTIME_CLASS(CMyObject) return -1; / 非法参数/ 用 pObject 对象来完成某项工作return 0; / 线程正常结束AfxBeginThread 的声明为:CWinThread* AfxBeginThread( AFX_THREADPRpOfnCThreadProc, LPVOIDpParam, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStack

12、Size = 0, DWORD dwCreateFlags = 0, LPSECURITY_ATTRIBUTES lpSecurityAttrs = NULL );参数pfnThreadProc是工作线程函数的地址。pParam是传递给线程函数的参 数。nPriority 是线程的优先级,一般是 THREAD_PRIORITY_NORM若为0,贝U 使用创建线程的优先级。 nStackSize 说明了线程的堆栈尺寸, 若为 0 则堆栈尺寸 与创建线程相同。 dwCreateFlags 指定了线程的初始状态, 如果为 0,那么线程在 创建后立即执行,如果为 CREATE_SUSPENDE贝线程在

13、创建后就被挂起。参数 IpSecurityAttrs 用来说明保密属性,一般为 0。函数返回新建的CWinThread对 象的指针。程序应该把AfxBeginThread返回的CWinThread指针保存起来,以便对创建 的线程进行控制。例如,可以调用 CWinThread:SetThreadPriority 来设置线程 的优先级,用 CWinThread:SuspendThread 来挂起线程。如果线程被挂起,那么 直到调用CWinThread:ResumeThread后线程才开始运行。如果要创建用户界面线程,那么必须从CWin Thread派生一个新类。事实上,代表进程主线程的CWinAp

14、p类就是CWinThread的派生类。派生类必须用 DECLARE_DYNCRE和TIMIPLEMENT_DYNCRE宏来声明和实现。需要重写派生类 的 Initln sta nee 、Exit In sta nee 、Run 等函数。可以使用 AfxBeginThread 函数的另一个版本来创建用户界面线程。 函数的声 明为:CWinThread* AfxBeginThread( CRuntimeCIass* pThreadCIass, int nPriority = THREAD_PRIORITY_NORMAL, UINT nStackSize = 0, DWORD dwCreateFIa

15、gs = 0, LPSECURITY_ATTRIBUTES IpSecurityAttrs = NULL );参数 pThreadClass 指向一个 CRuntimeClass 对象,该对象是用 RUNTIME_CLA宏从CWinThread的派生类创建的。其它参数以及函数的返回值与 第一个版本的 AfxBeginThread 是一样的。当发生下列事件之一时,线程被终止:线程调用 ExitThread 。线程函数返回,即线程隐含调用了 ExitThread 。ExitProcess 被进程的任一线程显示或隐含调用。用线程的句柄调用 TerminateThread 。用进程句柄调用 Termi

16、nateProcess 。线程的同步多线程的使用会产生一些新的问题,主要是如何保证线程的同步执行。多线程应用程序需要使用同步对象和等待函数来实现同步。12.2.1 为什么需要同步由于同一进程的所有线程共享进程的虚拟地址空间,并且线程的中断是汇编 语言级的,所以可能会发生两个线程同时访问同一个对象(包括全局变量、共享 资源、API函数和MFCM象等)的情况,这有可能导致程序错误。例如,如果一 个线程在未完成对某一大尺寸全局变量的读操作时,另一个线程又对该变量进行 了写操作,那么第一个线程读入的变量值可能是一种修改过程中的不稳定值。属于不同进程的线程在同时访问同一内存区域或共享资源时,也会存在同样

17、 的问题。因此,在多线程应用程序中,常常需要采取一些措施来同步线程的执行。需 要同步的情况包括以下几种:在多个线程同时访问同一对象时,可能产生错误。例如,如果当一个线程正在读 取一个至关重要的共享缓冲区时,另一个线程向该缓冲区写入数据,那么程序的 运行结果就可能出错。程序应该尽量避免多个线程同时访问同一个缓冲区或系统 资源。在Windows 95环境下编写多线程应用程序还需要考虑重入问题。Windows NT是真正的 32位操作系统,它解决了系统重入问题。 而 Wi n d ows 95由于继承了 Windows 3.x 的部分 16位代码,没能够解决重入问题。这意味着在 Windows 95

18、中两个线 程不能同时执行某个系统功能, 否则有可能造成程序错误, 甚至会造成系统崩溃。应用程序应该尽量避免发生两个以上的线程同时调用同一个Windows API函数的情况。由于大小和性能方面的原因,MFCM象在对象级不是线程安全的,只有在类级才 是。也就是说,两个线程可以安全地使用两个不同的CString对象,但同时使用同一个CString对象就可能产生问题。如果必须使用同一个对象,那么应该采取 适当的同步措施。多个线程之间需要协调运行。例如,如果第二个线程需要等待第一个线程完成到 某一步时才能运行,那么该线程应该暂时挂起以减少对CPU勺占用时间,提高程序的执行效率。当第一个线程完成了相应的步

19、骤后,应该发出某种信号来激活第 二个线程。12.2.2等待函数Win32 API提供了一组能使线程阻塞其自身执行的等待函数。这些函数只有 在作为其参数的一个或多个同步对象(见下小节)产生信号时才会返回。在超过规 定的等待时间后,不管有无信号,函数也都会返回。在等待函数未返回时,线程 处于等待状态,此时线程只消耗很少的CPU寸间。使用等待函数即可以保证线程的同步,又可以提高程序的运行效率。最常用 的等待函数是WaitForSingleObject ,该函数的声明为:DWORD WaitForSi ngleObject(HANDLE hHan dle, DWORD dwMilliseco nds)

20、;参数hHandle是同步对象的句柄。参数dwMilliseconds 是以毫秒为单位的超 时间隔,如果该参数为0,那么函数就测试同步对象的状态并立即返回,如果该 参数为INFINITE,则超时间隔是无限的。函数的返回值在表12.1中列出。表 12.1 WaitForSi ngleObject的返回值返回值 含义WAIT FAILED函数失败WAIT OBJECT 0指定的同步对象处于有信号的状态|WAIT ABANDONED拥有一个mutex的线程已经中断了,但未释放该 MUTExHWAIT TIMEOUT超时返回,并且同步对象无信号|函数WaitForMultipleObjects可以同时

21、监测多个同步对象,该函数的声明为:DWORWaitForMultipleObjects(DWORD nCount, CONSHANDLEIpHandles, BOOL bWaitAII, DWORD dwMilliseco nds );参数nCount是句柄数组中句柄的数目。lpHandles代表一个句柄数组。 bWaitAll说明了等待类型,如果为TRUE那么函数在所有对象都有信号后才返回, 如果为FALSE则只要有一个对象变成有信号的,函数就返回。函数的返回值在 表12.2中列出。参数dwMilliseconds是以毫秒为单位的超时间隔,如果该参数 为0那么函数就测试同步对象的状态并立即返

22、回,如果该参数为INFINITE,则超时间隔是无限的。表 12.2 WaitForMultipleObjects 的返回值返回值说明WAIT_OBJECT_0 WAIT_OBJECT_O+nCou nt-1若bWaitAll为TRUE则返回值表明所有对象都是有 信号的。如果bWaitAll为FALSE则返回值减去 WAIT OBJECT ffi是数组中有信号对象的最小索引。WAIT_ABANDONED_0WAIT_ABANDONED_ 0+nCoun t-1若bWaitAll为TRUE则返回值表明所有对象都有信号,但有一个mutex被放弃了。若bWaitAll为FALSE 则返回值减去WAIT

23、_ABANDONED_是被放弃mutex在 对象数组中的索引。WAIT_TIMEOUT超时返回。12.2.3同步对象同步对象用来协调多线程的执行,它可以被多个线程共享。线程的等待函数 用同步对象的句柄作为参数,同步对象应该是所有要使用的线程都能访问到的。 同步对象的状态要么是有信号的,要么是无信号的。同步对象主要有三种:事件、 mutex和信号灯。事件对象(Event)是最简单的同步对象,它包括有信号和无信号两种状态。在 线程访问某一资源之前,也许需要等待某一事件的发生,这时用事件对象最合适。 例如,只有在通信端口缓冲区收到数据后,监视线程才被激活。事件对象是用CreateEvent函数建立的

24、。该函数可以指定事件对象的种类和 事件的初始状态。如果是手工重置事件,那么它总是保持有信号状态,直到用 ResetEvent函数重置成无信号的事件。如果是自动重置事件,那么它的状态在单 个等待线程释放后会自动变为无信号的。用SetEvent可以把事件对象设置成有信 号状态。在建立事件时,可以为对象起个名字,这样其它进程中的线程可以用 Ope nEve nt函数打开指定名字的事件对象句柄。mutex对象的状态在它不被任何线程拥有时是有信号的,而当它被拥有时则 是无信号的。mutex对象很适合用来协调多个线程对共享资源的互斥访问(mutually exclusive) 。线程用CreateMute

25、x函数来建立mutex对象,在建立 mutex时,可以为对象 起个名字,这样其它进程中的线程可以用Ope nMutex函数打开指定名字的 mutex对象句柄。在完成对共享资源的访问后,线程可以调用ReleaseMutex来释放mutex,以便让别的线程能访问共享资源。如果线程终止而不释放mutex,则认为该mutex被废弃。信号灯对象维护一个从0开始的计数,在计数值大于0时对象是有信号的, 而在计数值为0时则是无信号的。信号灯对象可用来限制对共享资源进行访问的 线程数量。线程用CreateSemaphore函数来建立信号灯对象,在调用该函数时, 可以指定对象的初始计数和最大计数。在建立信号灯时

26、也可以为对象起个名字, 别的进程中的线程可以用Ope nSem aphorei数打开指定名字的信号灯句柄。一般把信号灯的初始计数设置成最大值。每次当信号灯有信号使等待函数返 回时,信号灯计数就会减1,而调用Releasesemaphore可以增加信号灯的计数。 计数值越小就表明访问共享资源的程序越多。除了上述三种同步对象外,表12.3中的对象也可用于同步。另外,有时可以 用文件或通信设备作为同步对象使用。表12.3可用于同步的对象对象描述变化通知由FindFirstChangeNotification函数建立,当在指疋目录中发生指定类型的变化时对象变成有信号的。控制台输入在控制台建立是被创建。

27、它是用 CONIN调用CreateFile函数返 回的句柄,或是GetStdHandle函数的返回句柄。如果控制台输 入缓冲区中有数据,那么对象是有信号的,如果缓冲区为空,贝U 对象疋无信号的。进程当调用CreateProcess建立进程时被创建。进程在运行时对象是 无信号的,当进程终止时对象是有信号的。线程当调用 Createprocess、CreateThread 或 CreateRemoteThread 函数创建新线程时被创建。在线程运行是对象是无信号的,在线 程终止时则是有信号的。当对象不再使用时,应该用 CloseHandle 函数关闭对象句柄清单 12.3 是一个使用事件对象的简单

28、例子, 在该例中,假设主线程要读取共 享缓冲区中的内容,而辅助线程负责向缓冲区中写入数据。两个线程使用了一个 hEvent 事件对象来同步。 在用 CreateEvent 函数创建事件对象句柄时, 指定该对 象是一个自动重置事件,其初始状态为有信号的。当线程要读写缓冲区时,调用 WaitForSingleObject 函数无限等待 hEvent 信号。如果 hEvent 无信号,则说明 另一线程正在访问缓冲区;如果有信号,则本线程可以访问缓冲区, WaitForSingleObject 函数在返回后会自动把 hEvent 置成无信号的, 这样在本线 程读写缓冲区时别的线程不会同时访问。 在完成

29、读写操作后, 调用 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 pPara

30、m )WaitForSingleObject(hEvent, INFINITE);WriteToBuf( );SetEvent( hEvent );return 0; / 线程正常结束12.2.4 关键节和互锁变量访问关键节 (Critical Seciton)与 mutex 的功能类似,但它只能由同一进程中的线程使用。关键节可以防止共享资源被同时访问。进程负责为关键节分配内存空间,关键节实际上是一个 CRITICAL_SECTION 型的变量,它一次只能被一个线程拥有。在线程使用关键节之前,必须调用 InitializeCriticalSection 函数将其初始化。如果线程中有一段关键的代

31、码不 希望被别的线程中断, 那么可以调用 EnterCriticalSection函数来申请关键节的所有权,在运行完关键代码后再用 LeaveCriticalSection 函数来释放所有权。 如 果在调用 EnterCriticalSection 时关键节对象已被另一个线程拥有, 那么该函数 将无限期等待所有权。利用互锁变量可以建立简单有效的同步机制。使用函数InterlockedIncrement 和 InterlockedDecrement 可以增加或减少多个线程共享 的一个 32位变量的值, 并且可以检查结果是否为 0。线程不必担心会被其它线程 中断而导致错误。如果变量位于共享内存中,

32、那么不同进程中的线程也可以使用 这种机制。串行通信与重叠 I/OWin 32 系统为串行通信提供了全新的服务。传统的OpenCom、mReadCom、mWriteComm CloseComr等函数已经过时,WM_COMMNOTF息也消失了。取而代 之的是文件 I/O 函数提供的打开和关闭通信资源句柄及读写操作的基本接口。新的文件I/O 函数(CreateFile、ReadFile、WriteFile 等)支持重叠式输入 输出,这使得线程可以从费时的 I/O 操作中解放出来,从而极大地提高了程序的 运行效率。12.3.1 串行口的打开和关闭Win 32 系统把文件的概念进行了扩展。无论是文件、通

33、信设备、命名管道、 邮件槽、 磁盘、还是控制台, 都是用 API 函数 CreateFile 来打开或创建的。 该函 数的声明为:HANDLE CreateFile(LPCTSTR lpFileName, / 文件名DWORD dwDesiredAccess, / 访问模式DWORD dwShareMode, /共享模式LPSECURITY_ATTRIBUTES lpSecurityAttributes, / 通常为 NULLDWORD dwCreationDistribution, / 创建方式DWORD dwFlagsAndAttributes, / 文件属性和标志HANDLE hTemp

34、lateFile / 临时文件的句柄,通常为 NULL);如果调用成功,那么该函数返回文件的句柄,如果调用失败,则函数返回INVALID_HANDLE_VALJUE如果想要用重叠I/O方式(参见12.3.3 )打开COM口,则一般应象清单12.4 那样调用 CreateFile 函数。注意在打开一个通信端口时,应该以独占方式打开, 另外要指定 GENERIC_READ3ENERIC_WRITEOPEN_EXISTIN和 FILE_ATTRIBUTE_NORM等属性。如果要打开重叠 I/O,则应该指定 FILE_FLAG_OVERLAPPB性。清单 12.4HANDLE hCom;DWORD d

35、wError;hCom=CreateFile( “COM”2 , / 文件名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 串行口的初始化在打开通信设备句柄后,常常需要对

36、串行口进行一些初始化工作。这需要通 过一个DCB结构来进行。DCB吉构包含了诸如波特率、每个字符的数据位数、奇 偶校验和停止位数等信息。在查询或配置置串行口的属性时,都要用DCB吉构来作为缓冲区。调用GetCommState函数可以获得串口的配置,该函数把当前配置填充到一个 DCB吉构中。一般在用 CreateFile 打开串行口后,可以调用 GetCommState函数 来获取串行口的初始配置。要修改串行口的配置,应该先修改DCB吉构,然后再调用SetCommState函数用指定的DCB结构来设置串行口。除了在DCB中的设置外,程序一般还需要设置I/O缓冲区的大小和超时。 Windows用I

37、/O缓冲区来暂存串行口输入和输出的数据,如果通信的速率较高, 则应该设置较大的缓冲区。调用 SetupComn函数可以设置串行口的输入和输出缓 冲区的大小。在用ReadFile和WriteFile 读写串行口时,需要考虑超时问题。如果在指定 的时间内没有读出或写入指定数量的字符, 那么 ReadFile 或 WriteFile 的操作就 会结束。要查询当前的超时设置应调用 GetCommTimeouts函数,该函数会填充一 个 COMMTIMEOI结构。调用 SetCommTimeoutS可以用某一个 COMMTIMEOUT构 的内容来设置超时。有两种超时:间隔超时和总超时。间隔超时是指在接收

38、时两个字符之间的最 大时延,总超时是指读写操作总共花费的最大时间。写操作只支持总超时,而读 操作两种超时均支持。用 COMMTIMEOL结构可以规定读/写操作的超时,该结构 的定义为:typedef struct _COMMTIMEOUTS DWORD ReadIntervalTimeout; / 读间隔超时DWORD ReadTotalTimeoutMultiplier; / 读时间系数DWORD ReadTotalTimeoutConstant; / 读时间常量DWORD WriteTotalTimeoutMultiplier; / 写时间系数DWORD WriteTotalTimeout

39、Constant; / 写时间常量 COMMTIMEOUTS,*LPCOMMTIMEOUTS;COMMTIMEOUT构的成员都以毫秒为单位。总超时的计算公式是:总超时=时间系数X要求读/写的字符数+时间常量例如,如果要读入 10 个字符,那么读操作的总超时的计算公式为:读总超时=ReadTotalTimeoutMultiplier X 10 + ReadTotalTimeoutCo nsta nt可以看出,间隔超时和总超时的设置是不相关的,这可以方便通信程序灵活 地设置各种超时。如果所有写超时参数均为 0,那么就不使用写超时。如果ReadIntervalTimeout 为 0,那么就不使用读间

40、隔超时,如果 ReadTotalTimeoutMultiplier 和 ReadTotalTimeoutConstant 都为 0,则不使用读 总超时。如果读间隔超时被设置成MAXDWO并且两个读总超时为0,那么在读一 次输入缓冲区中的内容后读操作就立即完成,而不管是否读入了要求的字符。在用重叠方式读写串行口时,虽然ReadFile和WriteFile在完成操作以前就 可能返回,但超时仍然是起作用的。在这种情况下,超时规定的是操作的完成时 间,而不是 ReadFile 和 WriteFile 的返回时间。清单 12.5 列出了一段简单的串行口初始化代码清单 12.5 打开并初始化串行口HAND

41、LE hCom;DWORD dwError;DCB dcb;COMMTIMEOUTS TimeOuts;hCom=CreateFile( “COM”2 , / 文件名GENERIC_READ | GENERIC_WRITE, /允/ 许读和写0, / 独占方式NULL,OPEN_EXISTING, /打开而不是创建FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED重叠方式NULL);if(hCom = = INVALID_HANDLE_VALUE)dwError=GetLastError( );. . . / 处理错误SetupComm( hCom, 10

42、24, 1024 ) / 缓冲区的大小为 1024TimeOuts. ReadIntervalTimeout=1000; TimeOuts.ReadTotalTimeoutMultiplier=500; TimeOuts.ReadTotalTimeoutConstant=5000;TimeOuts.WriteTotalTimeoutMultiplier=500;TimeOuts.WriteTotalTimeoutConstant=5000;SetCommTimeouts(hCom, &TimeOuts); / 设置超时GetCommState(hCom, &dcb);dcb.BaudRate=

43、2400; / 波特率为 2400 dcb.ByteSize=8; / 每个字符有 8 位 dcb.Parity=NOPARITY; / 无校验 dcb.StopBits=ONESTOPBIT; / 一个停止位 SetCommState(hCom, &dcb);12.3.3 重叠 I/O在用 ReadFile 和 WriteFile 读写串行口时,既可以同步执行,也可以重叠(异 步)执行。在同步执行时,函数直到操作完成后才返回。这意味着在同步执行时 线程会被阻塞,从而导致效率下降。在重叠执行时,即使操作还未完成,调用的 函数也会立即返回。费时的 I/O 操作在后台进行,这样线程就可以干别的事情

44、。 例如,线程可以在不同的句柄上同时执行 I/O 操作,甚至可以在同一句柄上同时 进行读写操作。“重叠”一词的含义就在于此。ReadFile 函数只要在串行口输入缓冲区中读入指定数量的字符, 就算完成操 作。而 WriteFile 函数不但要把指定数量的字符拷入到输出缓冲中,而且要等这 些字符从串行口送出去后才算完成操作。ReadFile 和 WriteFile 函数是否为执行重叠操作是由 CreateFile 函数决定 的。如果在调用 CreateFile 创建句柄时指定了 FILE_FLAG_OVERLAPP标志,那 么调用 ReadFile 和 WriteFile 对该句柄进行的读写操作

45、就是重叠的, 如果未指定 重叠标志,则读写操作是同步的。函数 ReadFile 和 WriteFile 的参数和返回值很相似。这里仅列出 ReadFile 函数的声明:BOOL ReadFile(HANDLE hFile, / 文件句柄LPVOID lpBuffer, / 读缓冲区DWORD nNumberOfBytesToRead, / 要求读入的字节数LPDWORD lpNumberOfBytesRead, / 实际读入的字节数LPOVERLAPPED IpOverlapped / 指向一个 OVERLAPPED勾);/若返回TRUE则表明操作成功需要注意的是如果该函数因为超时而返回,那么

46、返回值是TRUE参数IpOverlapped在重叠操作时应该指向一个 OVERLAPPED构,如果该参数为NULL 那么函数将进行同步操作,而不管句柄是否是由FILE_FLAG_OVERLAPP标志建立 的。当ReadFile和WriteFile 返回FALSE时,不一定就是操作失败,线程应该调 用 GetLastError 函数分析返回的结果。 例如,在重叠操作时如果操作还未完成函 数就返回,那么函数就返回 FALSE而且GetLastError函数返回ERROR_IO_PENDUNG在使用重叠I/O时,线程需要创建OVERLAPPED构以供读写函数使用。 OVERLAPPED构最重要的成员

47、是hEvent,hEvent是一个事件对象句柄,线程应 该用 CreateEvent 函数为 hEvent 成员创建一个手工重置事件, hEvent 成员将作 为线程的同步对象使用。如果读写函数未完成操作就返回,就那么把hEvent成员 设置成无信号的。操作完成后(包括超时), hEvent 会变成有信号的。如果GetLastError函数返回ERROR_IO_PENDIN则说明重叠操作还为完成, 线程可以等待操作完成。 有两种等待办法:一种办法是用象 WaitForSingleObject 这样的等待函数来等待 OVERLAPPED构的hEvent成员,可以规定等待的时间, 在等待函数返回后

48、,调用 GetOverlappedResult 。另一种办法是调用 GetOverlappedResult函数等待,如果指定该函数的 bWait参数为TRUE那么该 函数将等待OVERLAPPED构的hEvent事件。GetOverlappedResult可以返回一 个OVERLAPPED构来报告包括实际传输字节在内的重叠操作结果。如果规定了读 /写操作的超时,那么当超过规定时间后, hEvent 成员会变成 有信号的。 因此,在超时发生后, WaitForSingleObject 和 GetOverlappedResult 都会结束等待。 WaitForSingleObject 的 dwMi

49、lliseconds 参数会规定一个等待超 时,该函数实际等待的时间是两个超时的最小值。注意 GetOverlappedResult 不 能设置等待的时限,因此如果 hEvent 成员无信号,则该函数将一直等待下去。在调用ReadFile和 WriteFile 之前,线程应该调用 ClearCommError函数清 除错误标志。该函数负责报告指定的错误和设备的当前状态。调用PurgeCommg数可以终止正在进行的读写操作,该函数还会清除输入或 输出缓冲区中的内容。1234通信事件在 Windows 95/NT中,WM_COMMNOT消息已经取消,在串行口产生一个通 信事件时,程序并不会收到通知

50、消息。线程需要调用WaitCommEve n函数来监视发生在串行口中的各种事件,该函数的第二个参数返回一个事件屏蔽变量,用来 指示事件的类型。线程可以用 SetCommMas建立事件屏蔽以指定要监视的事件, 表12.4列出了可以监视的事件。调用GetCommMas可以查询串行口当前的事件屏 蔽。表12.4通信事件事件屏蔽含义EV BREAK检测到一个输入中断EV CTSCTS信号发生变化EV_DSRDSF信号发生变化EV ERR发生行状态错误EV RING检测到振铃信号EV RLSDRLSD(CD信号发生变化EV RXCHAR输入缓冲区接收到新字符EV RXFLAG输入缓冲区收到事件字符EV

51、TXEMPTY发送缓冲区为空WaitCommEve n即可以同步使用,也可以重叠使用。如果串口是用FILE_FLAG_OVERLAPP标志打开的,那么 WaitCommEven就进行重叠操作,此时 该函数需要一个OVERLAPPED构。线程可以调用等待函数或GetOverlappedResult函数来等待重叠操作的完成。当指定范围内的某一事件发生后,线程就结束等待并把该事件的屏蔽码设置 到事件屏蔽变量中。需要注意的是,WaitCommEve n只检测调用该函数后发生的 事件。例如,如果在调用 WaitCommEve n前在输入缓冲区中就有字符,贝U不会因 为这些字符而产生EV_RXCHAR件。

52、如果检测到输入的硬件信号(如 CTS RTS和CD信号等)发生了变化,线程 可以调用GetCommMaskStatu函数来查询它们的状态。而用EscapeCommFunction 函数可以控制输出的硬件信号(如 DTR和RTS信号)。一个通信演示程序为了使读者更好地掌握本章的概念,这里举一个具体实例来说明问题。如图 12.1所示,例子程序名为Terminal,是一个简单的TTY终端仿真程序。读者可以 用该程序打开一个串行口,该程序会把用户的键盘输入发送给串行口,并把从串 口接收到的字符显示在视图中。用户通过选择File-Connect命令来打开串行口, 选择File-Disconnect 命令

53、则关闭串行口。图12.1 Termi nal终端仿真程序当用户选择 File-Settings.命令时,会弹出一个 Communication settings对话框,如图12.2所示。该对话框主要用来设置串行口,包括端口、波特率、每 字节位数、校验、停止位数和流控制。图 12.2 Commu nication sett in gs对话框通过该对话框也可以设置TTY终端仿真的属性,如果选择 NewLine (自动换 行),那么每当从串口读到回车符(r )时,视图中的正文就会换行,否则, 只有在读到换行符(n )时才会换行。如果选择Local echo (本地回显),那 么发送的字符会在视图中显

54、示出来。终端仿真程序的特点是数据的传输没有规律。因为键盘输入速度有限,所以 发送的数据量较小,但接收的数据源是不确定的,所以有可能会有大量数据高速 涌入的情况发生。根据Terminal的这些特性,我们在程序中创建了一个辅助工作 者线程专门来监视串行口的输入。由于写入串行口的数据量不大,不会太费时, 所以在主线程中完成写端口的任务是可以的,不必另外创建线程。现在就让我们开始工作。请读者按下面几步进行: 用AppWizard建立一个名为Terminal的MFC应用程序。在MFCAppWizard对话框 的第1步选择Single document,在第4步去掉Docking toolbar的选择,在

55、第6 步把 CTerminalView 的基类改为 CEditView 。在Terminal工程的资源视图中打开IDR_MAINFRAM菜单资源。去掉Edit菜单和View菜单,并去掉File菜单中除Exit以外的所有菜单项。然后在 File菜单中 加入三个菜单项,如表12.5所示。表12.5新菜单项标题IDSett in gs.ID FILE SETTINGSConnectID FILE CONNECT用ClassWizard为CTerminalDoc类创建三个与上表菜单消息对应的命令处理函 数,使用缺省的函数名。为ID_FILE_CONNEC和 ID_FILE_DISCONNEC命令创建命

56、 令更新处理函数。另外,用 ClassWizard为该类加入 CanCloseFrame成员函数。用ClassWizard为CTermi nalView类创建On Char函数,该函数用来把用户键入的 字符向串行口输出。新建一个对话框模板资源,令其ID为IDD_COMSETTING请按图12.2和表12.6 设计对话框模板。表12.6通信设置对话框中的主要控件控件ID属性设置Base options 组框缺省标题为 Base optionsPort组合框IDC_PORTDrop List,不选Sort,初始列表 为 COM、COM2 COM3 COM4Baud rate组合框IDC_BAUDDrop List,不选Sort,初始列表 为 300、600、1200、2400、9600、 14400、19200、38400、57600Data bits 组合框IDC_DATABITSDrop

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论