2015-08-16 121 views
5

背景

一位客戶問我爲什麼他們的C#應用​​程序(我們稱之爲XXX,由一名已經逃離現場的顧問提供)很片面,並修復它。該應用程序通過串行連接控制測量設備。有時設備會提供連續的讀數(顯示在屏幕上),有時應用程序需要停止連續測量並進入命令響應模式。C# - 如何切換從串口讀取哪個線程?

如何

對於連續測量,XXX使用System.Timers.Timer爲串行輸入的後臺處理。當定時器觸發時,C#使用其池中的某個線程運行定時器的ElapsedEventHandler。 XXX的事件處理程序使用具有幾秒超時的阻止commPort.ReadLine(),然後在有用測量到達串行端口時回撥給代理。這部分工作正常,但...

當其停止實時測量和命令設備做不同的事情時,應用程序試圖通過設置計時器的Enabled = false掛起GUI線程的後臺處理。當然,這只是設置一個防止進一步事件的標誌,並且已經在等待串行輸入的後臺線程繼續等待。然後GUI線程向設備發送命令,並嘗試讀取回復 - 但回覆由後臺線程接收。現在背景線程變得混亂,因爲它不是預期的測量。 GUI線程同時變得困惑,因爲它沒有收到預期的命令回覆。現在我們知道XXX爲什麼如此片面。

可能的方法1

在另一個類似的應用程序,我用了一個System.ComponentModel.BackgroundWorker線程自由運行測量。暫停後臺處理我確實在GUI線程兩兩件事:

  1. 呼叫CancelAsync方法的線程上,並且
  2. 呼叫commPort.DiscardInBuffer(),這會導致一個掛起(阻塞,等待)COMPORT讀取在後臺線程拋出一個System.IO.IOException "The I/O operation has been aborted because of either a thread exit or an application request.\r\n"

在後臺線程中,我發現這個異常並及時清理,並且所有工作都按預期進行。不幸的是DiscardInBuffer在另一個線程的阻塞讀取中引發異常沒有記錄的行爲,我可以找到任何地方,我討厭依賴無證行爲。它的工作原理是因爲內部DiscardInBuffer調用Win32 API PurgeComm,它會中斷阻止讀取(已記錄的行爲)。

可能方法2

直接使用BaseClass Stream.ReadAsync方法,有監視器取消標記,使用中斷背景IO的支持方式。

由於要接收的字符數是可變的(以換行符結尾),並且框架中不存在ReadAsyncLine方法,所以我不知道這是否可行。我可以單獨處理每個字符,但性能會受到影響(可能無法在慢速機器上運行,除非線程終止位已在框架內的C#中實現)。

可能的方法3

創建鎖定「我有串行端口」。沒有人從端口讀取,寫入或丟棄輸入,除非他們擁有鎖(包括在後臺線程中重複阻塞讀取)。將後臺線程中的超時值切換爲1/4秒以獲得可接受的GUI響應,而無需太多開銷。

問題

有沒有人有經過驗證的解決方案來處理這個問題? 如何幹淨地停止串口的後臺處理? 我用Google搜索了幾十篇文章,感嘆C#SerialPort類,但還沒有找到一個好的解決方案。

在此先感謝!

+0

你是不是專注於真正的問題,它是System.Timers.Timer。擺脫它,並使用同步計時器來代替。 –

+0

對不起漢斯,我沒有關注。沒有可能的方法1-3使用System.Timers.Timer;你在暗示什麼? –

回答

2

MSDN文章爲SerialPort類中明確規定:

如果SerialPort對象就變成了讀操作過程中受阻,不中止線程。相反,要麼關閉基礎流處置SerialPort對象。

所以從我的角度來看,最好的方法是第二個,async讀取並逐步檢查換行符。正如你所說,每個字符的檢查是非常大的性能損失,我建議你調查ReadLine implementation的一些想法如何更快地執行此操作。請注意,他們使用SerialPort類的NewLine屬性。

我想還需要注意的是,沒有ReadLineAsync方法默認as the MSDN states

默認情況下,ReadLine方法將阻塞,直到收到一條線。如果此行爲不可取,請將ReadTimeout屬性設置爲任何非零值,以強制ReadLine方法在端口上沒有可用線路時拋出TimeoutException

所以,可能是在您的包裝可以實現類似的邏輯,所以如果有一些特定的時間沒有行結束您的Task將取消。此外,你應該注意這一點:

由於SerialPort類緩存數據,幷包含在 的BaseStream屬性不,這兩個可能會發生衝突約 有多少字節可供讀取數據流。該BytesToRead屬性可以 表示有要讀取的字節,但這些字節可能無法 訪問包含在BaseStream財產因爲 他們已經緩衝至SerialPort類流。

所以,再一次,我建議你實現與異步讀取每個讀取後檢查一些包裝邏輯,是有行尾與否,應阻止,並將其包裝內async方法,該方法將取消Task過了一段時間。

希望這會有所幫助。

0

好的,這是我所做的...評論將不勝感激,因爲C#對我而言仍然有些新鮮!

它瘋狂地有多個線程試圖同時訪問串口(或任何資源,尤其是異步資源)。要解決起來沒有一個完全重寫這個應用程序,我介紹了一個鎖SerialPortLockObject保證獨家串行端口訪問如下:

  • 的GUI線程持有SerialPortLockObject除非它運行的後臺操作。
  • SerialPort類被封裝,以致任何未持有SerialPortLockObject的線程讀取或寫入都會引發異常(幫助查找多個爭用錯誤)。
  • 定時器類被封裝(類SerialOperationTimer),以便通過獲取SerialPortLockObject來調用後臺工作器函數。 SerialOperationTimer一次只允許運行一個定時器(幫助發現幾個錯誤,其中GUI在啓動不同的定時器之前忘記停止後臺處理)。這可以通過使用定時器工作的特定線程來改進,該線程在定時器處於活動狀態的整個時間內都保持鎖定(但是仍然會工作更多;編碼System.Timers.Timer從線程池中運行工作者函數)。
  • 當SerialOperationTimer停止時,它會禁用底層定時器並刷新串行端口緩衝區(引發任何阻塞的串行端口操作的異常,如上面可能的方法1所述)。然後SerialPortLockObject被GUI線程重新獲取。

這裏的包裝爲SerialPort

/// <summary> CheckedSerialPort class checks that read and write operations are only performed by the thread owning the lock on the serial port </summary> 
// Just check reads and writes (not basic properties, opening/closing, or buffer discards). 
public class CheckedSerialPort : SafePort /* derived in turn from SerialPort */ 
{ 
    private void checkOwnership() 
    { 
     try 
     { 
      if (Monitor.IsEntered(XXX_Conn.SerialPortLockObject)) return; // the thread running this code has the lock; all set! 
      // Ooops... 
      throw new Exception("Serial IO attempted without lock ownership"); 
     } 
     catch (Exception ex) 
     { 
      StringBuilder sb = new StringBuilder(""); 
      sb.AppendFormat("Message: {0}\n", ex.Message); 
      sb.AppendFormat("Exception Type: {0}\n", ex.GetType().FullName); 
      sb.AppendFormat("Source: {0}\n", ex.Source); 
      sb.AppendFormat("StackTrace: {0}\n", ex.StackTrace); 
      sb.AppendFormat("TargetSite: {0}", ex.TargetSite); 
      Console.Write(sb.ToString()); 
      Debug.Assert(false); // lets have a look in the debugger NOW... 
      throw; 
     } 
    } 
    public new int ReadByte()          { checkOwnership(); return base.ReadByte(); } 
    public new string ReadTo(string value)       { checkOwnership(); return base.ReadTo(value); } 
    public new string ReadExisting()        { checkOwnership(); return base.ReadExisting(); } 
    public new void Write(string text)        { checkOwnership(); base.Write(text); } 
    public new void WriteLine(string text)       { checkOwnership(); base.WriteLine(text); } 
    public new void Write(byte[] buffer, int offset, int count)  { checkOwnership(); base.Write(buffer, offset, count); } 
    public new void Write(char[] buffer, int offset, int count)  { checkOwnership(); base.Write(buffer, offset, count); } 
} 

而這裏的包裝爲System.Timers.Timer

/// <summary> Wrap System.Timers.Timer class to provide safer exclusive access to serial port </summary> 
class SerialOperationTimer 
{ 
    private static SerialOperationTimer runningTimer = null; // there should only be one! 
    private string name; // for diagnostics 
    // Delegate TYPE for user's callback function (user callback function to make async measurements) 
    public delegate void SerialOperationTimerWorkerFunc_T(object source, System.Timers.ElapsedEventArgs e); 
    private SerialOperationTimerWorkerFunc_T workerFunc; // application function to call for this timer 
    private System.Timers.Timer timer; 
    private object workerEnteredLock = new object(); 
    private bool workerAlreadyEntered = false; 

    public SerialOperationTimer(string _name, int msecDelay, SerialOperationTimerWorkerFunc_T func) 
    { 
     name = _name; 
     workerFunc = func; 
     timer = new System.Timers.Timer(msecDelay); 
     timer.Elapsed += new System.Timers.ElapsedEventHandler(SerialOperationTimer_Tick); 
    } 

    private void SerialOperationTimer_Tick(object source, System.Timers.ElapsedEventArgs eventArgs) 
    { 
     lock (workerEnteredLock) 
     { 
      if (workerAlreadyEntered) return; // don't launch multiple copies of worker if timer set too fast; just ignore this tick 
      workerAlreadyEntered = true; 
     } 
     bool lockTaken = false; 
     try 
     { 
      // Acquire the serial lock prior calling the worker 
      Monitor.TryEnter(XXX_Conn.SerialPortLockObject, ref lockTaken); 
      if (!lockTaken) 
       throw new System.Exception("SerialOperationTimer " + name + ": Failed to get serial lock"); 
      // Debug.WriteLine("SerialOperationTimer " + name + ": Got serial lock"); 
      workerFunc(source, eventArgs); 
     } 
     finally 
     { 
      // release serial lock 
      if (lockTaken) 
      { 
       Monitor.Exit(XXX_Conn.SerialPortLockObject); 
       // Debug.WriteLine("SerialOperationTimer " + name + ": released serial lock"); 
      } 
      workerAlreadyEntered = false; 
     } 
    } 

    public void Start() 
    { 
     Debug.Assert(Form1.GUIthreadHashcode == Thread.CurrentThread.GetHashCode()); // should ONLY be called from GUI thread 
     Debug.Assert(!timer.Enabled); // successive Start or Stop calls are BAD 
     Debug.WriteLine("SerialOperationTimer " + name + ": Start"); 
     if (runningTimer != null) 
     { 
      Debug.Assert(false); // Lets have a look in the debugger NOW 
      throw new System.Exception("SerialOperationTimer " + name + ": Attempted 'Start' while " + runningTimer.name + " is still running"); 
     } 
     // Start background processing 
     // Release GUI thread's lock on the serial port, so background thread can grab it 
     Monitor.Exit(XXX_Conn.SerialPortLockObject); 
     runningTimer = this; 
     timer.Enabled = true; 
    } 

    public void Stop() 
    { 
     Debug.Assert(Form1.GUIthreadHashcode == Thread.CurrentThread.GetHashCode()); // should ONLY be called from GUI thread 
     Debug.Assert(timer.Enabled); // successive Start or Stop calls are BAD 
     Debug.WriteLine("SerialOperationTimer " + name + ": Stop"); 

     if (runningTimer != this) 
     { 
      Debug.Assert(false); // Lets have a look in the debugger NOW 
      throw new System.Exception("SerialOperationTimer " + name + ": Attempted 'Stop' while not running"); 
     } 
     // Stop further background processing from being initiated, 
     timer.Enabled = false; // but, background processing may still be in progress from the last timer tick... 
     runningTimer = null; 
     // Purge serial input and output buffers. Clearing input buf causes any blocking read in progress in background thread to throw 
     // System.IO.IOException "The I/O operation has been aborted because of either a thread exit or an application request.\r\n" 
     if(Form1.xxConnection.PortIsOpen) Form1.xxConnection.CiCommDiscardBothBuffers(); 
     bool lockTaken = false; 
     // Now, GUI thread needs the lock back. 
     // 3 sec REALLY should be enough time for background thread to cleanup and release the lock: 
     Monitor.TryEnter(XXX_Conn.SerialPortLockObject, 3000, ref lockTaken); 
     if (!lockTaken) 
      throw new Exception("Serial port lock not yet released by background timer thread "+name); 
     if (Form1.xxConnection.PortIsOpen) 
     { 
      // Its possible there's still stuff in transit from device (for example, background thread just completed 
      // sending an ACQ command as it was stopped). So, sync up with the device... 
      int r = Form1.xxConnection.CiSync(); 
      Debug.Assert(r == XXX_Conn.CI_OK); 
      if (r != XXX_Conn.CI_OK) 
       throw new Exception("Cannot re-sync with device after disabling timer thread " + name); 
     } 
    } 

    /// <summary> SerialOperationTimer.StopAllBackgroundTimers() - Stop all background activity </summary> 
    public static void StopAllBackgroundTimers() 
    { 
     if (runningTimer != null) runningTimer.Stop(); 
    } 

    public double Interval 
    { 
     get { return timer.Interval; } 
     set { timer.Interval = value; } 
    } 

} // class SerialOperationTimer 
+0

你有正確的想法,但使你鎖定在公開上的對象被認爲是反模式,並有很好的理由(因爲任何代碼都可以劫持監視器)。相反,我會自定義SafePort類型,它是線程安全的並執行自己的鎖定,並重構所有代碼以強制訪問裸端口,以便使用該SafePort類型的公共方法(所有鎖定的東西都是私有的)。更清潔,而且不需要驗證訪問以監視哪些看起來非常倒退等。有關c#和.NET中的線程原語的更多信息,請查看以下優秀資源:www.albahari.com/threading/ – Mahol25