有位網友在我之前的文章回應了一個問題『從網路上及書籍收集到現在,針對RS232於外接裝置的通訊都是採取timer的機制實現詢問或控制的效果。但用此方法是無法有效實現在一定時間內增加詢問的次數。請問有什麼方法可以控制整個輪詢是在ms等級的實現?』因為這問題實在太大哉問了,在回應中恐怕沒法講得齊全,而且可以趁機多寫一篇文章,所以就另闢一篇文來談談這個問題。
有位網友在我之前的文章回應了一個問題『從網路上及書籍收集到現在,針對RS232於外接裝置的通訊都是採取timer的機制實現詢問或控制的效果。但用此方法是無法有效實現在一定時間內增加詢問的次數。請問有什麼方法可以控制整個輪詢是在ms等級的實現?』因為這問題實在太大哉問了,在回應中恐怕沒法講得齊全,而且可以趁機多寫一篇文章,所以就另闢一篇文來談談這個問題。
首先要先解釋的是我不太瞭解ms等級是指10ms以內還是1ms以內,不過其實狀況也差不多,這不會影響到後續要討論的議題,我純粹是想多扯個兩句而已。
回歸到寫程式的本質來,當我們遇到一個很嚴苛的要求時,通常我的第一個反應是『這個需求有必要被達成嗎?』在這邊我必須假設這個需求是有必要的,否則就沒辦法繼續掰下去。既然這個是必要的,大部份的人通常下一步就是直接想『那該如何達成?』;但這不是我的風格--因為我是念企管系的,所以下一步通常我會去思考『達成這個目的的限制是什麼?』,這就是今天要談的主題了—Serial Port通訊效率的限制問題。
個人概略整理了一下關於這些限制的問題,主要可分為三個大類:(1) 流程上的限制 (2) 硬體、作業系統與環境的限制 (3) 程式語言與設計上的限制,這三大類別之間其實也含有著某種程度的交互關係。
(1) 流程上的限制
這個限制第一個牽扯到你的通訊模型與資料量,簡單來說通訊程式大概有兩種模式:
(1-1-1) 射後不理模式:也就是發送端完全不管接收端的回應,反正只要一直發送訊號出去就好了,這個情況下流程的限制會降到最低,在完全不考慮對於使用者介面的影響下甚至你可以用一個迴圈拼命地發送資料,尤其當你發送的資料很小(ex: 每次只發送一個Byte),你就可以不用思索流程上的問題了。(1-1-2) 命令<-->回應模式:如果程式發送的一個命令出去還必須等待回應,那這個循環就有很多地方會造成延遲 (如果你認為用非同步可以解決,那你就誤解我的意思了,因為這個的描述是指整個循環完成的時間,也就是說要等到回應執行完畢才能算是整個循環完成 ),例如:接收端在收到命令後需要多久時間才能回應、資料量與傳輸率的搭配等等。當你的流程是這樣的時候,就要認真地考慮所有在這個通訊模型會造成影響的任何一個物體或事件。
第二個則是你在發送與接收命令的同時,系統的執行上還會處理哪些東西
(1-2-1) 如果程式在流程上還要處理畫面,那一定會有某種程度降低效率,因為它要花點時間來更新畫面,就算使用多執行緒,切換執行緒還是需要時間的。更何況錯誤的多執行緒設計可能會讓效率更差。
(1-2-2) 如果還要寫入資料到硬碟,那就更慘,因為硬碟的I/O會花費更多的時間。
(2) 硬體、作業系統與環境的限制
(2-1) 硬體:硬體的效率直接牽動了程式的運作的時間,而影響比較大的一端還不是PC端,因為現在的PC已經夠快了,但設備端的硬體卻有重量級的影響。舉個例子來說,我常常通訊的另一端設備是以8051為主,並且只有很小的記憶體,在這種狀況下真的很難期待它可以多快的回應你的命令。這還有另外一個問題是設備端本身在韌體程式上的設計能不能達到最佳效率也是我們寫PC端程式的人所不能控制的 (除非設備端的韌體也是你自己寫的,否則你除了嘆氣之外還真的沒有什麼辦法)。
(2-2) 作業系統:PC端程式的運作多半還是要受作業系統的限制,因為現在的作業系統光開起來就要用掉一堆資源,如果你的作業系統越忙,那可以想見在多工狀態下可以分配給你程式的資源就會變少,如果可以把作業系統弄到最小,小到只為你的程式與串列埠服務,那對執行效率一定會有正面的影響。另一個作業系統造成的影響在於作業系統在設計上是如何處理週邊/記憶體/CPU之間的關係,這個設計會影響到Serial Port收到資料後是如何透過作業系統又回應給你的程式,不過老實講,除非你願意自己寫一套作業系統,這個影響也不是我們有能力改變的。
(2-3) 環境:環境會造成的影響大略是在幾個方面
(2-3-1) 傳輸率的設定,想當然爾傳輸率是越大越好,但是這又會扯到下一個要談到的通訊品質,因為世事不能盡如人意,就像你不可能用 Cat. 3 的網路線來跑Giga-Bits 網路一樣。
(2-3-2) 通訊品質:如果通訊品質不良,重送的機率就會增加,因此就要用時間來彌補正確性,影響通訊品質的狀況會出現在幾個地方 (a) 串列埠晶片 (b) 線路品質,如線徑、長度與隔離 (c) 外部環境,如重電強磁的影響;而以上的因素就會對於傳輸率的設定造成限制。
(3) 程式語言與設計上的限制
眾所皆知 .Net Managed Code 程式是依賴CLR (詳情請參閱MSDN文件庫 Common Language Runtime) 運作,不過這世上沒有完美的事情,CLR幫我們處理掉很多程式設計上要考慮的頭痛問題,尤其是記憶體的管理,寫過C語言的人應該都有受過那種自己要寫程式去管理記憶體的痛苦,在.Net 程式上要這麼麻煩的機率實在很低;但即使Microsoft 已經盡力在改善CLR運作效率的問題,畢竟比不上Native Code可以展現的力量,所以某些效率上的問題就取決於你用哪種程式語言開發。
如果我今天要開發一個程式是對效率有『極致』要求的,我一定不會選用C#、VB.NET或是Java來寫,最有可能的選擇是 C/C++甚至於Assembly,因為這些語言才能有效提升程式的執行效率;更誇張一點的說,甚至應該自己設計一個整合晶片,把程式嵌在晶片中來運作。
那如果我們把理想縮小一點,用C#或VB.NET來達成『較有限地極致』效率做為討論:
(3-1) 關於Timer的問題,我想你會看到很多使用Timer的範例是因為它比較方便簡單,可以少解釋很多事情;基本上我在專案中除非有特定要求是固定時間輪詢,否則我不怎麼常使用Timer,大部份還是以使用Thread搭配迴圈為主。但.Net 上的Timer對寫程式會造成怎樣的限制呢?第一個當然是它精確度的問題,這和主機板本身也有關,而它本身的確也存在著這樣的問題,有幾篇文章對於.Net 的時間精確度有不錯的說明 (不僅僅針對Timer):
Code Project : Timer surprises, and how to avoid them
黑暗執行緒 : KB-測試Thread.Sleep的精確度 ,KB-Thread.Sleep, 別賴床!
鄭子璉 : MyWait 函數 ,[VBNET]定時整點動作,VB6/VBNET CPU 資源釋出
Tmer的問題還不僅在精確度而已,如果你有看過我寫的時間人系列就會發現Timer有個特性是當它在執行某一個Method時,如果這個Method還沒完成而下一次的Tick卻已經來到,它會將前面的程序暫止,這意思就是說,不論你的Timer可以到多快,只要執行的Method時間超過Timer的間隔時間,這個程式必然會導致非預期的結果,因為事情根本沒做完。
(3-2) 若有必要,直接呼叫Win32 API而捨棄使用.Net Framework中的類別也會增強程式效率,例如使用Kernel32.dll中的CreateFile、OpenFile等等函式來替代.Net SerialPort類別,不過這樣就會增加寫程式的難度了,因為本來沒幾行的東西會變成一大堆。
(3-3) 型別轉換也是影響效率的原因之一,如果可以,請儘量減少型別轉換。
(3-4) 減少對硬碟執行I/O的時間,假設今天你在串列埠是轉換成RS485/422, 接了一大串設備,而你必須根據某份表 (例如設備的ID表) 來輪詢設備,在效率上有要求的時候就得要先把這份表讀進記憶體中,之後每一個輪迴都是靠著這份記憶體中的資料來執行,而不是每一個輪迴都得要從資料表或檔案中讀取。
(3-5) 改進程式中的演算法,好的演算法有助於節省執行時間。
(3-6) 族繁不及備載,因為再寫下去可能我要找出版社幫我出一本書了。
不曉得以上的討論是否有解答到這位網友的問題,亦或是其實我已經把大家搞得更糊塗了,總之不論你寫哪方面的程式,而專案中有特定的某種嚴苛需求時,我們必須要先找出限制這個需求被達成的大部份問題,從裡面去抽絲剝繭,才有可能找出解決的方案。因為一個真實在使用的程式會受到很多外在因素的干擾,講回前面的例子,如果單純只有快,那就寫一個迴圈拼命送很小量的資料也許是可能的,但真實世界的需求絕對沒有如此單純的,因此就要思考很多很多的東西,而不僅僅是單點的能不能做而已。