多線程串口通信 MFC CSerialPort


寫在前面:

         晚上應該繼續完成未寫完的代碼,但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 

至此,經測試問題全部解決,記錄一下。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM