2015-08-16 8 views
5

Un cliente mi ha chiesto di scoprire perché la loro applicazione C# (la chiameremo XXX, consegnata da un consulente che è fuggito dalla scena) è così squilibrata e la risolve. L'applicazione controlla un dispositivo di misurazione su una connessione seriale. A volte il dispositivo fornisce letture continue (che vengono visualizzate sullo schermo) e talvolta l'app deve interrompere le misurazioni continue e passare alla modalità comando-risposta.C# - Come consegnare quale thread legge dalla porta seriale?

Come NON per farlo

Per misurazioni continue, XXX utilizza System.Timers.Timer per il fondo trattamento di ingresso seriale. Quando il timer scatta, C# esegue il timer ElapsedEventHandler usando del filo dal suo pool. Il gestore di eventi di XXX utilizza un blocco commPort.ReadLine() con un secondo timeout, quindi richiama un delegato quando arriva una misura utile sulla porta seriale. Questa porzione funziona bene, tuttavia ...

Quando è il momento di interrompere le misurazioni in tempo reale e di comandare al dispositivo di fare qualcosa di diverso, l'applicazione tenta di sospendere l'elaborazione in background dal thread della GUI impostando il timer Enabled = false. Ovviamente, questo imposta semplicemente un flag che impedisce ulteriori eventi, e un thread in background che attende l'input seriale continua ad attendere. Il thread della GUI invia quindi un comando al dispositivo e tenta di leggere la risposta, ma la risposta viene ricevuta dal thread in background. Ora il thread di sfondo diventa confuso poiché non è la misura prevista. Nel frattempo, il thread GUI diventa confuso poiché non ha ricevuto la risposta del comando prevista. Ora sappiamo perché XXX è così friabile.

Metodo Possibile 1

In un'altra applicazione simile, ho usato un filo System.ComponentModel.BackgroundWorker per misure free-running. Per sospendere l'elaborazione in background Ho fatto due cose nel thread GUI:

  1. chiamata al metodo CancelAsync sul filo, e
  2. chiamata commPort.DiscardInBuffer(), che provoca una in sospeso (bloccato, in attesa) Comport lette in thread in background per gettare un System.IO.IOException "The I/O operation has been aborted because of either a thread exit or an application request.\r\n".

Nel thread in background rilevo questa eccezione e pulisco prontamente, e tutto funziona come previsto. Sfortunatamente DiscardInBuffer provocare l'eccezione nella lettura del blocco di un altro thread non è un comportamento documentato ovunque riesca a trovarlo, e odio affidarmi a comportamenti non documentati. Funziona perché internamente DiscardInBuffer chiama PurgeComm API Win32, che interrompe la lettura del blocco (comportamento documentato).

Metodo Possibile 2

Direttamente utilizzare il metodo BaseClass Stream.ReadAsync, con un token di cancellazione monitor, usando un metodo supportato di interrompere sfondo IO.

Poiché il numero di caratteri da ricevere è variabile (terminato da una nuova riga) e non esiste un metodo ReadAsyncLine nel framework, non so se questo è possibile. Potrei elaborare ogni personaggio singolarmente ma prenderebbe un risultato in termini di prestazioni (potrebbe non funzionare su macchine lente, a meno che, naturalmente, il bit di terminazione di linea sia già implementato in C# all'interno del framework).

metodo possibile 3

Creare un blocco "Ho la porta seriale". Nessuno legge, scrive o scarta l'input dalla porta a meno che non abbia il blocco (inclusa la ripetizione del blocco letto nel thread in background). Taglia i valori di timeout nel thread in background a 1/4 secondo per una reattività GUI accettabile senza troppo sovraccarico.

Domanda

Qualcuno ha una soluzione collaudata per affrontare questo problema? Come si può arrestare in modo pulito l'elaborazione in background della porta seriale? Ho cercato su Google e letto dozzine di articoli lamentando la classe C# SerialPort, ma non ho trovato una buona soluzione.

Grazie in anticipo!

+0

Non stanno concentrando sul problema reale, è System.Timers.Timer. Sbarazzati di esso e usa invece un timer sincrono. –

+0

Mi dispiace Hans, non seguo. Nessuno dei possibili metodi 1-3 utilizza System.Timers.Timer; cosa stai suggerendo? –

risposta

2

articolo MSDN per la classe SerialPort chiaramente:

Se un oggetto SerialPort viene bloccato durante un'operazione di lettura, NON interrompere il filo. Invece, chiude lo stream di base o smaltire l'oggetto SerialPort.

Quindi l'approccio migliore, dal mio punto di vista, è seconda, con async lettura e passo dopo passo il controllo per il carattere di fine riga. Come hai detto, il controllo per ogni char è una perdita di prestazioni molto grande, ti suggerisco di investigare sullo ReadLine implementation per alcune idee su come eseguire questo più velocemente. Si noti che usano la proprietà NewLine della classe SerialPort.

Voglio anche sottolineare che non esiste un metodo per default ReadLineAsyncas the MSDN states:

Per impostazione predefinita, il metodo ReadLine bloccherà fino alla ricezione di una linea. Se questo comportamento non è opportuno, impostare la proprietà ReadTimeout su qualsiasi valore diverso da zero per forzare il metodo ReadLine a generare un TimeoutException se una linea non è disponibile sulla porta.

Quindi, può essere, nel vostro involucro è possibile implementare la logica simile, così il vostro Task annullerà se non c'è fine linea in un certo momento. Inoltre, si dovrebbe notare che questo:

perché il SerialPort dati buffer di classe, e il flusso di contenuti in proprietà BaseStream non, i due potrebbero entrare in conflitto su come molti byte sono disponibili per la lettura. La proprietà BytesToRead può indicare che ci sono byte da leggere, ma questi byte potrebbero non essere accessibili al flusso contenuto nella proprietà BaseStream perché sono stati tamponata alla classe SerialPort.

Così, ancora una volta, suggerisco di implementare una logica involucro con lettura asincrono e il controllo dopo ogni lettura, ci sono di fine linea o no, che dovrebbe essere il blocco, e avvolgerlo dentro async metodo, che si annulla Task dopo un po 'di tempo.

Spero che questo aiuti.

0

OK, ecco cosa ho fatto ... I commenti sarebbero apprezzati in quanto C# è ancora un po 'nuovo per me!

È pazzesco avere più thread che tentano di accedere alla porta seriale contemporaneamente (o qualsiasi risorsa, in particolare una risorsa asincrona). Per risolvere questa applicazione senza una completa riscrittura, ho introdotto un blocco SerialPortLockObject per garantire l'accesso esclusivo porta seriale come segue:

  • filo L'interfaccia grafica tiene SerialPortLockObject tranne quando si ha un funzionamento in background in esecuzione.
  • La classe SerialPort viene avvolta in modo tale che qualsiasi lettura o scrittura da parte di un thread che non regge SerialPortLockObject genera un'eccezione (ha aiutato a trovare diversi bug di contesa).
  • La classe del timer è incapsulata (classe SerialOperationTimer) in modo che la funzione di lavoro in background venga chiamata tra parentesi acquisendo SerialPortLockObject. SerialOperationTimer consente di eseguire un solo timer alla volta (ha aiutato a trovare diversi bug in cui la GUI ha dimenticato di interrompere l'elaborazione in background prima di avviare un timer diverso). Questo potrebbe essere migliorato usando un thread specifico per il lavoro con timer, con quel thread che tiene il blocco per tutto il tempo in cui il timer è attivo (ma sarebbe ancora più lavoro, come codificato System.Timers.Timer esegue la funzione worker dal pool di thread).
  • Quando un SerialOperationTimer viene arrestato, disabilita il timer sottostante e svuota i buffer della porta seriale (provocando un'eccezione da qualsiasi operazione di porta seriale bloccata, come spiegato nel possibile metodo 1 sopra). Quindi SerialPortLockObject viene riacquisito dal thread della GUI.

Ecco il wrapper per 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); } 
} 

Ed ecco il wrapper per 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

Hai l'idea giusta, ma rendere l'oggetto che stai bloccando in pubblico è considerato un anti-pattern e per una buona ragione (dal momento che qualsiasi codice può dirottare il monitor). Invece, vorrei creare un tipo di SafePort personalizzato che sia thread-safe ed esegua il proprio lock, e refactoring di tutto il codice per forzare l'accesso alla porta naked per utilizzare metodi pubblici di quel tipo SafePort (con tutte le funzionalità di locking private). Molto più pulito, e non c'è bisogno di verificare l'accesso al monitor che sembra molto arretrato, ecc. Per ulteriori informazioni sul threading delle primitive in C# e .NET, controlla questa eccellente risorsa: www.albahari.com/threading/ – Mahol25