C#-如何切换从串行端口读取的线程? 背景

C#-如何切换从串行端口读取的线程? 背景,c#,multithreading,asynchronous,serial-port,C#,Multithreading,Asynchronous,Serial Port,一位客户让我找出他们的C#应用程序(我们称之为XXX,由一位逃离现场的顾问交付)如此脆弱的原因,并修复它。该应用程序通过串行连接控制测量设备。有时,设备提供连续读数(显示在屏幕上),有时应用程序需要停止连续测量并进入命令响应模式 怎么不做呢 对于连续测量,XXX使用System.Timers.Timer对串行输入进行后台处理。当计时器启动时,C#使用其池中的某个线程运行计时器的ElapsedEventHandler。XXX的事件处理程序使用阻塞commPort.ReadLine(),并有几秒钟的

一位客户让我找出他们的C#应用程序(我们称之为XXX,由一位逃离现场的顾问交付)如此脆弱的原因,并修复它。该应用程序通过串行连接控制测量设备。有时,设备提供连续读数(显示在屏幕上),有时应用程序需要停止连续测量并进入命令响应模式

怎么不做呢 对于连续测量,XXX使用
System.Timers.Timer
对串行输入进行后台处理。当计时器启动时,C#使用其池中的某个线程运行计时器的
ElapsedEventHandler
。XXX的事件处理程序使用阻塞
commPort.ReadLine()
,并有几秒钟的超时,然后在串行端口上出现有用的测量值时回调委托。这部分很好,但是

当停止实时测量并命令设备执行不同的操作时,应用程序将通过设置计时器的
Enabled=false
,尝试从GUI线程暂停后台处理。当然,这只是设置了一个防止进一步事件的标志,已经在等待串行输入的后台线程继续等待。然后,GUI线程向设备发送一个命令,并尝试读取回复,但后台线程会收到回复。现在背景线程变得混乱,因为它不是预期的度量。同时,GUI线程变得混乱,因为它没有收到预期的命令回复。现在我们知道为什么XXX如此脆弱了

可能的方法1 在另一个类似的应用程序中,我使用
System.ComponentModel.BackgroundWorker
线程进行自由运行的测量。为了暂停后台处理,我在GUI线程中做了两件事:

  • 在线程上调用
    CancelAsync
    方法,然后
  • 调用
    commPort.DiscardInBuffer()
    ,这会导致后台线程中的挂起(阻止、等待)组件读取引发
    System.IO.IOException“由于线程退出或应用程序请求,I/O操作已中止。\r\n”
    在后台线程中,我捕捉到这个异常并迅速清理,所有工作都如期进行。不幸的是,
    DiscardInBuffer
    在另一个线程的阻塞读取中引发异常在我能找到的任何地方都不是记录的行为,我讨厌依赖未记录的行为。它之所以能工作,是因为在内部,
    DiscardInBuffer
    调用Win32 API PurgeComm,它会中断阻塞读取(记录的行为)

    可能的方法2 直接使用
    基类Stream.ReadAsync
    方法,使用支持的中断后台IO的方式,使用监视器取消令牌

    因为要接收的字符数是可变的(以换行符结尾),并且框架中不存在
    ReadAsyncLine
    方法,所以我不知道这是否可行。我可以单独处理每个字符,但会受到性能影响(可能无法在速度较慢的机器上工作,当然,除非行终止位已经在框架内的C#中实现)

    可能的方法3 创建一个锁“我有串行端口”。除非拥有锁(包括在后台线程中重复阻塞读取),否则没有人从端口读取、写入或丢弃输入。将后台线程中的超时值缩短到1/4秒,以获得可接受的GUI响应,而不会产生太多开销

    问题: 有人有解决这个问题的行之有效的方法吗? 如何才能干净地停止串行端口的后台处理? 我在谷歌上搜索并阅读了几十篇抱怨C#
    SerialPort类的文章,但没有找到一个好的解决方案

    提前谢谢

    MSDN针对该类的文章明确指出:

    如果
    SerialPort
    对象在读取操作期间被阻塞,请勿中止线程。相反,关闭基本流处理
    串行端口
    对象

    因此,在我看来,最好的方法是第二种方法,即
    async
    读取并逐步检查行尾字符。正如您所说的,每个字符的检查都会造成很大的性能损失,我建议您研究一下如何更快地执行。请注意,它们使用
    SerialPort
    类的属性

    我还要注意,默认情况下没有
    readlinesync
    方法:

    默认情况下,
    ReadLine
    方法将阻塞,直到收到一行。
    如果此行为不可取,请将
    ReadTimeout
    属性设置为任何非零值,以强制
    ReadLine
    方法在端口上没有可用行时抛出
    TimeoutException

    因此,可能在包装器中,您可以实现类似的逻辑,因此如果在某个给定时间内没有行结束,您的
    任务将取消。此外,您还应注意:

    因为
    SerialPort
    类缓冲数据,而
    BaseStream
    属性没有,这两个属性可能会在如何执行方面发生冲突 许多字节可供读取。
    BytesToRead
    属性可以 指示存在要读取的字节,但这些字节可能不存在 可访问
    基本流
    属性中包含的流
    ,因为 它们已缓冲到
    SerialPort

    因此,我再次建议您使用异步读取实现一些包装器逻辑,并在每次读取后检查是否有行尾,这应该是阻塞的,并将其包装在
    async
    方法中,该方法将在一段时间后取消
    任务


    希望这能有所帮助。

    好的,下面是我所做的。。。由于C#仍在使用中,请发表意见
    /// <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); }
    }
    
    /// <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