本章我们将以工业控制和嵌入式系统中运用极为广泛的串口通信为例讲述多线程的典型应用。
而网络通信也是多线程应用最广泛的领域之一,所以本章的最后一节也将对多线程网络通信进行简短的描述。
1.串口通信
在工业控制系统中,工控机(一般都基于PC Windows平台)经常需要与单片机通过串口进行通信。因此,操作和使用PC的串口成为大多数单片机、嵌入式系统领域工程师必须具备的能力。
串口的使用需要通过三个步骤来完成的:
(1) 打开通信端口;
(2) 初始化串口,设置波特率、数据位、停止位、奇偶校验等参数。为了给读者一个直观的印象,下图从Windows的"控制面板->系统->设备管理器->通信端口(COM1)"打开COM的设置窗口:
(3) 读写串口。
在WIN32平台下,对通信端口进行操作跟基本的文件操作一样。
创建/打开COM资源
下列函数如果调用成功,则返回一个标识通信端口的句柄,否则返回-1:
HADLE CreateFile(PCTSTR lpFileName, //通信端口名,如"COM1" |
获得/设置COM属性
下列函数可以获得COM口的设备控制块,从而获得相关参数:
BOOL WINAPI GetCommState( |
如果要调整通信端口的参数,则需要重新配置设备控制块,再用WIN32 API SetCommState()函数进行设置:
BOOL SetCommState( |
DCB结构包含了串口的各项参数设置,如下:
typedef struct _DCB |
读写串口
在读写串口之前,还要用PurgeComm()函数清空缓冲区,并用SetCommMask ()函数设置事件掩模来监视指定通信端口上的事件,其原型为:
BOOL SetCommMask( |
串口上可能发生的事件如下表所示:
值 |
事件描述 |
EV_BREAK |
A break was detected on input. |
EV_CTS |
The CTS (clear-to-send) signal changed state. |
EV_DSR |
The DSR(data-set-ready) signal changed state. |
EV_ERR |
A line-status error occurred. Line-status errors are CE_FRAME, CE_OVERRUN, and CE_RXPARITY. |
EV_RING |
A ring indicator was detected. |
EV_RLSD |
The RLSD (receive-line-signal-detect) signal changed state. |
EV_RXCHAR |
A character was received and placed in the input buffer. |
EV_RXFLAG |
The event character was received and placed in the input buffer. The event character is specified in the device's DCB structure, which is applied to a serial port by using the SetCommState function. |
EV_TXEMPTY |
The last character in the output buffer was sent. |
在设置好事件掩模后,我们就可以利用WaitCommEvent()函数来等待串口上发生事件,其函数原型为:
BOOL WaitCommEvent( |
我们可以在发生事件后,根据相应的事件类型,进行串口的读写操作:
BOOL ReadFile(HANDLE hFile, //标识通信端口的句柄 |
2.工程实例
下面我们用第1节所述API实现一个多线程的串口通信程序。这个例子工程(工程名为MultiThreadCom)的界面很简单,如下图所示:
|
它是一个多线程的应用程序,包括两个工作者线程,分别处理串口1和串口2。为了简化问题,我们让连接两个串口的电缆只包含RX、TX两根连线(即不以硬件控制RS-232,串口上只会发生EV_TXEMPTY、EV_RXCHAR事件)。
在工程实例的BOOL CMultiThreadComApp::InitInstance()函数中,启动并设置COM1和COM2,其源代码为:
BOOL CMultiThreadComApp::InitInstance() |
此后我们在对话框CMultiThreadComDlg的初始化函数OnInitDialog中启动两个分别处理COM1和COM2的线程:
BOOL CMultiThreadComDlg::OnInitDialog() |
两个串口COM1和COM2对应的线程处理函数等待串口上发生事件,并根据事件类型和自身缓冲区是否有数据要发送进行相应的处理,其源代码为:
DWORD WINAPI Com1ThreadProcess(HWND hWnd//主窗口句柄) |
线程控制函数中所操作的com1Data和com2Data是与串口对应的数据结构struct tagSerialPort的实例,这个数据结构是:
typedef struct tagSerialPort |
3.多线程串口类
使用多线程串口通信更方便的途径是编写一个多线程的串口类,例如Remon Spekreijse编写了一个CSerialPort串口类。仔细分析这个类的源代码,将十分有助于我们对先前所学多线程及同步知识的理解。
3.1类的定义
#ifndef __SERIALPORT_H__ |
3.2类的实现
3.2.1构造函数与析构函数
进行相关变量的赋初值及内存恢复:
CSerialPort::CSerialPort() |
3.2.2核心函数:初始化串口
在初始化串口函数中,将打开串口,设置相关参数,并创建串口相关的用户控制事件,初始化临界区(Critical Section),以成队的EnterCriticalSection()、LeaveCriticalSection()函数进行资源的排它性访问:
BOOL CSerialPort::InitPort(CWnd *pPortOwner, |
3.3.3核心函数:串口线程控制函数
串口线程处理函数是整个类中最核心的部分,它主要完成两类工作:
(1)利用WaitCommEvent函数对串口上发生的事件进行获取并根据事件的不同类型进行相应的处理;
(2)利用WaitForMultipleObjects函数对串口相关的用户控制事件进行等待并做相应处理。
UINT CSerialPort::CommThread(LPVOID pParam) |
下列三个函数用于对串口线程进行启动、挂起和恢复:
// |
3.3.4读写串口
下面一组函数是用户对串口进行读写操作的接口:
// |
3.3.5控制接口
应用程序员使用下列一组public函数可以获取串口的DCB及串口上发生的事件:
// |
3.3.6错误处理
// |
仔细分析Remon Spekreijse的CSerialPort类对我们理解多线程及其同步机制是大有益处的,从http://codeguru.earthweb.com/network/serialport.shtml我们可以获取CSerialPort类的介绍与工程实例。另外,电子工业出版社《Visual C++/Turbo C串口通信编程实践》一书的作者龚建伟也编写了一个使用CSerialPort类的例子,可以从http://www.gjwtech.com/scomm/sc2serialportclass.htm获得详情。
4.多线程网络通信
在网络通信中使用多线程主要有两种途径,即主监控线程和线程池。
4.1主监控线程
这种方式指的是程序中使用一个主线程监控某特定端口,一旦在这个端口上发生连接请求,则主监控线程动态使用CreateThread派生出新的子线程处理该请求。主线程在派生子线程后不再对子线程加以控制和调度,而由子线程独自和客户方发生连接并处理异常。
使用这种方法的优点是:
(1)可以较快地实现原型设计,尤其在用户数目较少、连接保持时间较长时有表现较好;
(2)主线程不与子线程发生通信,在一定程度上减少了系统资源的消耗。
其缺点是:
(1)生成和终止子线程的开销比较大;
(2)对远端用户的控制较弱。
这种多线程方式总的特点是"动态生成,静态调度"。
4.2线程池
这种方式指的是主线程在初始化时静态地生成一定数量的悬挂子线程,放置于线程池中。随后,主线程将对这些悬挂子线程进行动态调度。一旦客户发出连接请求,主线程将从线程池中查找一个悬挂的子线程:
(1)如果找到,主线程将该连接分配给这个被发现的子线程。子线程从主线程处接管该连接,并与用户通信。当连接结束时,该子线程将自动悬挂,并进人线程池等待再次被调度;
(2)如果当前已没有可用的子线程,主线程将通告发起连接的客户。
使用这种方法进行设计的优点是:
(1)主线程可以更好地对派生的子线程进行控制和调度;
(2)对远程用户的监控和管理能力较强。
虽然主线程对子线程的调度要消耗一定的资源,但是与主监控线程方式中派生和终止线程所要耗费的资源相比,要少很多。因此,使用该种方法设计和实现的系统在客户端连接和终止变更频繁时有上佳表现。
这种多线程方式总的特点是"静态生成,动态调度"。