C#-如何切换哪个线程从串行端口读取

本文关键字:串行端口 读取 线程 何切换 C#- | 更新日期: 2023-09-27 18:08:17

背景

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

如何去做

对于连续测量,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(),这将导致后台线程中读取的挂起(阻塞、等待(组件抛出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响应,而不会有太多开销。

问题

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

提前感谢!

C#-如何切换哪个线程从串行端口读取

MSDN关于SerialPort类的文章明确指出:

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

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

我还想注意的是,默认情况下没有ReadLineAsync方法,因为MSDN声明:

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

因此,在包装器中,您可以实现类似的逻辑,因此如果在某个给定时间内没有行尾,则Task将取消。此外,您应该注意:

因为SerialPort类缓冲数据BaseStream属性没有,两者可能会在如何许多字节可供读取。BytesToRead属性可以指示有字节要读取,但这些字节可能不是可访问BaseStream属性中包含的流,因为它们已经被缓冲到CCD_ 31类。

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

希望这能有所帮助。

好吧,这是我所做的。。。如有意见,我们将不胜感激,因为C#对我来说还是有些新鲜!

多个线程试图同时访问串行端口(或任何资源,尤其是异步资源(,这太疯狂了。为了在不完全重写的情况下修复这个应用程序,我引入了一个锁SerialPortLockObject来保证独占串行端口访问,如下所示:

  • GUI线程保持SerialPortLockObject,除非它有后台操作在运行
  • SerialPort类被封装,因此不包含SerialPortLockObject的线程的任何读取或写入都会引发异常(有助于发现几个争用错误(
  • 定时器类被封装(类SerialOperationTimer(,从而通过获取SerialPortLockObject来调用后台工作者函数。SerialOperationTimer一次只允许运行一个计时器(有助于发现GUI在启动不同计时器之前忘记停止后台处理的几个错误(。这可以通过使用一个特定的线程进行定时器工作来改进,该线程在定时器活动的整个时间内都保持锁(但仍将是更多的工作;因为编码的System.Timers.Timer从线程池运行工作函数(
  • 当SerialOperationTimer停止时,它会禁用底层计时器并刷新串行端口缓冲区(如上面可能的方法1所述,从任何被阻止的串行端口操作中引发异常(。然后GUI线程重新获取CCD_ 42

这是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