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所示。这样做既可以保证工作实时进行,又可以使程序能有限地响应用户输入,但此时程序实际上已不能再为用户干别的事情了。
//清单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参数可用来接收必要的参数。
清单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。
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。线程不必担心会被其它线程中断而导致错误。如果变量位于共享内存中,那么不同进程中的线
程也可以使用这种机制。
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信号)。
原作出处