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

Dav*_*ler 5 c# multithreading asynchronous serial-port

背景

一位客户让我弄清楚为什么他们的C#应用​​程序(我们称之为XXX,由一位逃离现场的顾问提供)是如此不稳定,并修复它.应用程序通过串行连接控制测量设备.有时设备会提供连续读数(显示在屏幕上),有时应用程序需要停止连续测量并进入命令响应模式.

怎么这样做

对于连续测量,XXX System.Timers.Timer用于串行输入的后台处理.当计时器触发时,C#ElapsedEventHandler使用其池中的某个线程运行计时器.XXX的事件处理程序使用具有几秒commPort.ReadLine()超时的阻塞,然后在有用的测量到达串行端口时回调给委托.这部分工作正常,但是......

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

可能的方法1

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

  1. CancelAsync在线程上调用方法,然后
  2. call 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课的文章,但还没有找到一个好的解决方案.

提前致谢!

VMA*_*Atm 2

该类的 MSDN 文章SerialPort明确指出:

如果SerialPort对象在读取操作期间被阻塞,请不要中止线程。相反,要么关闭基础流,要么丢弃该SerialPort对象。

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

我还想指出, MSDN 指出默认情况下没有ReadLineAsync方法:

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

因此,也许在您的包装器中您可以实现类似的逻辑,因此Task如果在某个给定时间内没有行结束,您将取消。另外,您还应该注意这一点:

由于SerialPort类缓冲数据,而属性中包含的流BaseStream则不缓冲数据,因此两者可能会在可供读取的字节数方面发生冲突。该BytesToRead属性可以指示有字节要读取,但这些字节可能无法被属性中包含的流访问BaseStream,因为它们已缓冲到SerialPort类中。

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

希望这可以帮助。