文/渔樵阿飞
CnComm是llbird开发的WINDOWS/WINCE 多线程串口通讯开源库,使用C++ (ANSI/UNICODE)开发,支持的平台包括WINDOWS(WIN98/NT/2000/XP/2003/Vista),WINCE 5.0 模拟器, Pocket PC 2003 模拟器,在BC++ 5(free tool); C++ BUILDER 4, 5, 6, X;EVC 4(sp4); G++ 3, 4; Intel C++ 7, 8, 9; VC++ 6(sp6), .NET, 2003, 2005等编译工具下编译测试通过,代码采用传统C++的继承机制, 采用VC命名风格(匈牙利),提供同步IO并发访问的支持,内存管理采用内存池技术,提供对于C++异常的支持,对于串口库的扩展,不推荐直接在本代码上修改, 应通过C++继承扩展机制扩展本代码。
CnComm多线程串口类的类结构如下,CnComm是定义的多线程串口类,CnComm::BlockBuffer类是根据通讯特点开发的缓冲区类,单向链表内存块,提供一些扩展以支持和API挂接,CnComm::InnerLock是自动锁类,用于函数内部,利用对象的生命周期完成锁定及解锁,CnComm::MfcException是一个异常处理类,用于MFC的异常处理,CnComm::BlockBuffer::Block是定义的缓冲区内存块,CnComm::BlockBuffer::InnerLock是定义的自动锁类,CnComm::BlockBuffer::Iterator是定义的缓冲区迭代器。在这个多线程的串口类中,定义了多个嵌套类。
现代C++使用RAII的机制,使用类来管理资源,在构造函数中分配资源,在析构函数中释放资源,这种方法管理资源基本上不会遇到什么问题,然而,动态内存的管理,一直就是一个烫手的山芋, C/C++提供了多种方式,C中的malloc/free,C++中的new/delete以及new[]/delete[],全局的::operator new和::operator delete,C++标准库中提供的allocator::allocate和allocator::deallocate,以及内存池的技术。
如果出现在多线程的情况下,并发访问的出现,还不止要解决好内存泄露方面的问题,对于多线程的race condition同样棘手,你必须考虑各种并发问题:如果在一个线程读取某个数据结构的同时,另一个线程正在更新同一个数据结构,除非你用适当的锁或无锁算法保护这个数据结构,保证适当的序列化操作,否则就会发生严重的混乱。通常我们通过加锁来避免对于数据的破坏,或者设计精妙的锁无关算法。在CnComm多线程的串口类中,作者使用内存池技术来管理内存块,利用锁结构来实现对于并发访问的控制。
1.单向链表内存块
在运行过程中,BlockBuffer内存池可能会有多个用来满足内存申请请求的内存块,这些内存块是从进程堆中开辟的一个较大的连续内存区域,它由一个Block结构体和可供分配的内存单元组成,所有内存块组成了一个内存块链表,BlockBuffer的头指针F_是这个链表的头,尾指针L_指向最后一个分配的Block内存块。对每个内存块,都可以通过其头部的Block结构体的N_成员访问紧跟在其后面的那个内存块。
源码如下:
//! 根据通讯特点开发的缓冲区类 单向链表内存块 有一些扩展以支持和API挂接
class BlockBuffer
{
public:
struct Block ;
struct Iterator ;
//! 友元
friend struct Iterator;
//! 锁定
void Lock()
{
::EnterCriticalSection(&C_);
}
//! 解锁
void Unlock()
{
::LeaveCriticalSection(&C_);
}
//! 自动锁
struct InnerLock
{
BlockBuffer* ptr;//!<对象指针
///锁定
InnerLock(BlockBuffer* p) : ptr(p)
{
if (ptr)
ptr->Lock();
}
///解锁
~InnerLock()
{
if (ptr)
ptr->Unlock();
}
};
BlockBuffer()
{
::InitializeCriticalSection(&C_);
S_ = 0, F_ = L_ = NULL, M_ = CN_COMM_BUFFER_MIN_BLOCK_SIZE;
}
virtual ~BlockBuffer()
{
Clear();
::DeleteCriticalSection(&C_);
}
protected:
//! 新建块 自动添加在尾部
Block* NewBlock(DWORD dwSize)
{
dwSize = dwSize < M_ ? M_ : dwSize;
Block * pNew = (Block *) new BYTE[sizeof(Block) - 4 + dwSize];
if (pNew)
{
memset(pNew, 0, sizeof(Block));
pNew->S_ = dwSize;
if (L_)
L_->N_ = pNew, L_ = pNew;
else
F_ = L_ = pNew;
}
return pNew;
}
Block* F_;//!< 头指针
Block* L_;//!< 尾指针
DWORD S_;//!< 大小
DWORD M_;//!< 块最小长度
CRITICAL_SECTION C_;//!< 锁结构
};
2.缓冲区内存块
每个缓冲区内存块由两部分组成,即一个Block结构体头和内存分配缓冲区。这些内存分配单元大小由S_记录,Block结构体维护该缓冲区内存块单元的信息。结构体头记录了内存块的开始偏移,结束偏移,块大小,下一块指针以及缓冲区块的指针。FreeSize返回这个内存块中还有多少个自由分配单元,而N_则记录下一个可供分配的单元的编号。
代码如下:
//! 缓冲区内存块
struct Block
{
DWORD B_; //!< 开始偏移
DWORD E_; //!< 结束偏移
DWORD S_; //!< 块大小 内存块最大值不限 内存块最小值由CN_COMM_BUFFER_MIN_BLOCK_SIZE决定
Block* N_; //!< 下一个块指针
BYTE P_[4]; //!< 缓冲区指针 实际大小由S_决定
//! 容量
DWORD Capacity(){return S_;}
//! 实际大小
DWORD Size(){return E_ - B_;}
//! 开始缓冲区指针
BYTE* Begin(){return P_ + B_;}
//! 末端缓冲区指针
BYTE* End(){return P_ + E_;}
//! 下一个块
Block* Next(){return N_;}
//! 是否空
bool IsEmpty(){return B_ == E_;}
//! 空闲大小
DWORD FreeSize(){return S_ - E_;}
};
3.CnComm串口类
在并发访问的时候,多个线程试图同时访问临界区,那么在有一个线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界区。所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection()的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的可能。在一个多线程的应用程序中,线程之间共享对象的问题是通过用这样一个对象联系临界区来解决的。每一个需要访问共享资源的客户需要获得临界区。多线程串口类CnComm是使用 Win32 下临界区的来实现同步控制。
class CnComm
{
public:
//! 临界区
struct InnerLock;
//! 缓冲区类
class BlockBuffer;
//! MFC异常
class MfcException;
//! WIN32:默认打开串口时启动监视线程,异步重叠方式
CnComm(DWORD dwOption = EN_THREAD | EN_OVERLAPPED)
{
Init();
SetOption(dwOption);
}
//! 析构 自动关闭串口
virtual ~CnComm()
{
Close();
Destroy();
}
//! 锁定
void Lock()
{
::EnterCriticalSection(&CS_);
}
//! 解锁
void Unlock()
{
::LeaveCriticalSection(&CS_);
}
//! 自动锁 用于函数内部 利用对象的生命周期完成锁定及解锁
struct InnerLock
{
CnComm* ptr;//!< CnComm 对象指针
//! 锁定
InnerLock(CnComm* p) : ptr(p)
{
ptr->Lock();
}
//! 解锁
~InnerLock()
{
ptr->Unlock();
}
};
//! 初始化
virtual void Init()
{
::InitializeCriticalSection(&CS_);
}
//! 析构
virtual void Destroy()
{
::DeleteCriticalSection(&CS_);
}
private:
//! 禁止拷贝构造
CnComm(const CnComm&);
//! 禁止赋值函数
CnComm &operator = (const CnComm&);
protected:
CRITICAL_SECTION CS_; //!< 临界互斥锁
};
这里我们需要确保不会造成死锁,要确保每一个进入临界区的资源最后都可以离开,不论是出现异常还是出现分支返回,通常要用到锁(lock)。
//! 自动锁 用于函数内部 利用对象的生命周期完成锁定及解锁
struct InnerLock
{
CnComm* ptr;//!< CnComm 对象指针
//! 锁定
InnerLock(CnComm* p) : ptr(p)
{
ptr->Lock();
}
//! 解锁
~InnerLock()
{
ptr->Unlock();
}
};
其用法如下:
通过C++的RAII机制保证无论在什么情况下都会解锁。不会造成死锁的情况。
DWORD SafeWrite(LPCVOID lpBuf, DWORD dwSize)
{
InnerLock lock(this);
return Write(lpBuf, dwSize);
}