寫在前面:
晚上應該繼續完成未寫完的代碼,但Chrome上打開的標簽實在太多了,約30個了,必須關掉一些,所以需要把自己看的整理一下然后關掉。本次主要寫點MFC環境下多線程串口通信相關的東西,這包括線程創建及控制、串口同步異步操作、內存非法訪問(或者說是線程同步)、線程通信、Windows消息響應過程等。
遇到問題:
項目中IO傳感器通信模塊之前直接寫在了主線程中,UI代碼和串口通信代碼攪合在一起,不利於后期維護,而且有個非常嚴重的問題,IO通信太忙導致整個系統比較卡,特別是當系統接上超過3個攝像機之后,MFC模態對話框使用Domodal()直接無法打開,卡住了,然后用戶就無法操作了,這個問題必須要解決。
解決方案:
單獨開辟一個線程來處理所有的串口通信,該IO線程和主線程(負責UI部分)通信,從而更新狀態,不能子線程中直接更新UI,參看《MFC最好不要在子線程中操控界面上的控件》。
具體步驟:
1.創立IO線程並完成消息響應
HANDLE hThread1 = CreateThread( NULL,0,IOControlProc,(LPVOID)(m_pCOMSerialPort),0,&m_dwIOControlThreadId );//創建IO線程 CloseHandle( hThread1 );//關閉線程句柄
其中
CSerialPort *m_pCOMSerialPort;//通信串口 DWORD m_dwIOControlThreadId;//線程ID
IOControlProc線程函數
DWORD WINAPI IOControlProc(LPVOID lpParameter)
{
CSerialPort *pSerialPort = (CSerialPort*)lpParameter;
UINT_PTR nHUMITimer = 0;
UINT_PTR nIOTimer = 0;
//::SetTimer(NULL,NULL,200,(TIMERPROC)TimerProc);
nHUMITimer = ::SetTimer(NULL,TEMPHUMICOMM_TIMER,500,(TIMERPROC)OnIOTimer);//溫濕度
nIOTimer =::SetTimer(NULL,IOCOMM_TIMER,100,(TIMERPROC)OnIOTimer);//IO
PIOTimerStru pHumiTimer= new IOTimerStru;
pHumiTimer->nIdEvent = TEMPHUMICOMM_TIMER;
pHumiTimer->pSerialPort = pSerialPort;
PIOTimerStru pIoTimer = new IOTimerStru;
pIoTimer->nIdEvent = IOCOMM_TIMER;
pIoTimer->pSerialPort = pSerialPort;
PIOTimerStru pIoControl = new IOTimerStru;
pIoControl->nIdEvent = TEMPHUMICOMM_TIMER + IOCOMM_TIMER;
pIoControl->pSerialPort = pSerialPort;
LPARAM lParam;
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
if (WM_TIMER == msg.message)
{
lParam = msg.lParam;//WM_TIMER回調函數的地址
if (nHUMITimer == msg.wParam)
{
msg.wParam = (WPARAM)pHumiTimer;
}
else if (nIOTimer == msg.wParam)
{
msg.wParam = (WPARAM)pIoTimer;
}
}
else if (WM_IOCOMMDATA == msg.message)
{
msg.message = WM_TIMER;
pIoControl->wParam = msg.wParam;
pIoControl->lParam = msg.lParam;
msg.wParam = (WPARAM)pIoControl;
msg.lParam = lParam;
}
DispatchMessage(&msg);
}
delete pHumiTimer;
delete pIoTimer;
delete pIoControl;
return 0;
}
這里,IO線程有自己的消息循環隊列(雖然沒有窗口),參看:《子線程里如何使用定時器》,把這里的代碼改成死循環的(參看《是否在子線程內使用SetTimer?》),如下
VOID CALLBACK TimerProc( HWND hwnd,
UINT uMsg,
UINT_PTR idEvent,
DWORD dwTime
){
//do some thing
return;
}
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
::SetTimer(NULL,NULL,200,(TIMERPROC)TimerProc);
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
我的消息響應函數寫成如下
VOID CALLBACK OnIOTimer(HWND hwnd,UINT uMsg,UINT_PTR idEvent,DWORD dwTime)
{
PIOTimerStru pStru = (PIOTimerStru)idEvent;
if (pStru->nIdEvent == TEMPHUMICOMM_TIMER)//溫濕度模塊
{
static bool bFlag = true;
if (bFlag)
{
IOSendData(pStru->pSerialPort,2,0x0A,0,0x03);
}
else
{
IOSendData(pStru->pSerialPort,2,0x0A,0,0x04);
}
bFlag = !bFlag;
}
else if (pStru->nIdEvent == IOCOMM_TIMER)//IO控制模塊
{
IOSendData(pStru->pSerialPort,1,0x10,0,0x03);
}
else if (pStru->nIdEvent == TEMPHUMICOMM_TIMER + IOCOMM_TIMER)//別的線程發送的指令
{
WORD highDeviceIndex = HIWORD(pStru->wParam);//設備號
WORD lowPortIndex = LOWORD(pStru->wParam);//設備1端口號,設備2指定溫度或濕度模塊
WORD highParam = HIWORD(pStru->lParam);//設備1指定狀態1或者0,設備2指定整數部分
WORD lowParam = LOWORD(pStru->lParam);//設備1為0,設備2指定小數部分
IOSendData(pStru->pSerialPort,(int)highDeviceIndex,(BYTE)highParam,(BYTE)lowParam,(BYTE)lowPortIndex);
}
return;
}
這里要注意一個問題,OnIOTimer中獲得的idEvent並不是我們設置的IOCOMM_TIMER或者TEMPHUMICOMM_TIMER,究其原因,參看《SetTimer在無窗口和有窗口線程的使用》,文中關於原因的解釋為“注:只有當hWnd參數為非空時,計時器的ID為設置的 nIDEvent, 系統為你自動生成一個計時器ID,可由返回時值獲取.”,可由MSDN得知。
那么我們的這個IOCOMM_TIMER或者TEMPHUMICOMM_TIMER,怎么傳遞過去呢?查看《怎么往SetTimer的回調函數傳遞參數》得知,我們可由msg.wParam傳遞。
那么對於我們自定義的消息TEMPHUMICOMM_TIMER + IOCOMM_TIMER,我也想讓OnIOTimer來處理怎么辦?直接修改msg.message = WM_TIMER;就可以了嘛?答案是不行的!
為什么呢?參看《消息循環中的TranslateMessage函數和DispatchMessage函數》,原來“如果參數lpmsg指向一個WM_TIMER消息,並且WM_TIMER消息的參數IParam不為NULL,則調用IParam指向的函數,而不是調用窗口程序。”,那么我們直接修改msg.lParam為OnIOTimer函數的地址就行了。這樣,所有消息響應完成了。
2.多線程訪問沖突(線程沖突)
遇到一個問題,系統有一個串口通信端口,IO線程直接使用了,然后我在主線程中也發送數據,然后問題出現了,有時候會出現非法訪問,跟蹤了一下,原來兩個線程使用了同一個串口通信緩沖區,主線程往里面壓入數據的時候,可能子線程已經釋放了該緩沖區,查詢文章《CSerialPort連續發送大量數據時出錯原因分析》,而我使用的CSerialPort是同步的,我嘗試修改類庫代碼,報錯太多,此方法放棄。最終,我的解決方法是把所有的串口IO通信全部交給子線程來做,那么主線程要做的事,可以通過發送消息給子線程,再由子線程代勞,主線程怎么發送消息給子線程?使用API函數PostThreadMessage來完成,第一個參數就是子線程的Id。
子線程收到數據后,進行驗證,驗證通過后,發消息給主線程,通知它更新界面。《主線程與子線程間通信解決辦法 - VC/MFC》
3.主線程退出前,關閉子線程
PostThreadMessage(m_dwIOControlThreadId,WM_QUIT,0,0); Sleep(100);//需要等待關閉掉
GetMessage有消息時且消息不為WM_QUIT時返回TRUE,如果有消息且為WM_QUIT則返回FALSE,沒有消息時不返回。
這里可參看《如何正確的關閉 MFC 線程》和《GetMessage和PeekMessage的聯系與區別以及用法 TranslateMessage與DispatchMessage 》
至此,經測試問題全部解決,記錄一下。
