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线程中做了两件事:
- 调用线程上的
CancelAsync
方法 - 调用
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
类,但没有找到一个好的解决方案。
提前感谢!
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