文/漁樵阿飛
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);
}