A customer asked me to find out why their C# application (we'll call it XXX, delivered by a consultant who has fled the scene) is so flaky, and fix it. The application controls a measurement device over a serial connection. Sometimes the device delivers continuous readings (which are displayed on screen), and sometimes the app needs to stop continuous measurements and go into command-response mode.
For continuous measurements, XXX uses System.Timers.Timer
for background processing of serial input. When the timer fires, C# runs the timer's ElapsedEventHandler
using some thread from its pool. XXX's event handler uses a blocking commPort.ReadLine()
with a several second timeout, then calls back to a delegate when a useful measurement arrives on the serial port. This portion works fine, however...
When its time to stop realtime measurements and command the device to do something different, the application tries to suspend background processing from the GUI thread by setting the timer's Enabled = false
. Of course, that just sets a flag preventing further events, and a background thread already waiting for serial input continues waiting. The GUI thread then sends a command to the device, and tries to read the reply – but the reply is received by the background thread. Now the background thread becomes confused as its not the expected measurement. The GUI thread meanwhile becomes confused as it didn't receive the command reply expected. Now we know why XXX is so flaky.
In another similar application, I used a System.ComponentModel.BackgroundWorker
thread for free-running measurements. To suspend background processing I did two things in the GUI thread:
CancelAsync
method on the thread, andcommPort.DiscardInBuffer()
, which causes a pending (blocked, waiting) comport read in the background thread to throw a System.IO.IOException "The I/O operation has been aborted because of either a thread exit or an application request.\r\n"
. In the background thread I catch this exception and clean up promptly, and all works as intended. Unfortunately DiscardInBuffer
provoking the exception in another thread's blocking read is not documented behavior anywhere I can find, and I hate relying on undocumented behavior. It works because internally DiscardInBuffer
calls the Win32 API PurgeComm, which interrupts the blocking read (documented behavior).
Directly use the BaseClass Stream.ReadAsync
method, with a monitor cancellation token, using a supported way of interrupting the background IO.
Because the number of characters to be received is variable (terminated by a newline), and no ReadAsyncLine
method exists in the framework, I don't know if this is possible. I could process each character individually but would take a performance hit (might not work on slow machines, unless of course the line-termination bit is already implemented in C# within the framework).
Create a lock "I've got the serial port". Nobody reads, writes, or discards input from the port unless they have the lock (including repeating the blocking read in background thread). Chop the timeout values in the background thread to 1/4 second for acceptable GUI responsiveness without too much overhead.
Does anybody have a proven solution to deal with this problem?
How can one cleanly stop background processing of the serial port?
I've googled and read dozens of articles bemoaning the C# SerialPort
class, but haven't found a good solution.
Thanks in advance!
MSDN article for the SerialPort
Class clearly states:
If a
SerialPort
object becomes blocked during a read operation, do not abort the thread. Instead, either close the base stream or dispose of theSerialPort
object.
So the best approach, from my point of view, is second one, with async
reading and step by step checking for the line-ending character. As you've stated, the check for each char is very big performance loss, I suggest you to investigate the ReadLine
implementation for some ideas how to perform this faster. Note that they use NewLine
property of SerialPort
class.
I want also to note that there is no ReadLineAsync
method by default as the MSDN states:
By default, the
ReadLine
method will block until a line is received. If this behavior is undesirable, set theReadTimeout
property to any non-zero value to force theReadLine
method to throw aTimeoutException
if a line is not available on the port.
So, may be, in your wrapper you can implement similar logic, so your Task
will cancel if there is no line end in some given time. Also, you should note this:
Because the
SerialPort
class buffers data, and the stream contained in theBaseStream
property does not, the two might conflict about how many bytes are available to read. TheBytesToRead
property can indicate that there are bytes to read, but these bytes might not be accessible to the stream contained in theBaseStream
property because they have been buffered to theSerialPort
class.
So, again, I suggest you to implement some wrapper logic with asynchronous read and checking after each read, are there line-end or not, which should be blocking, and wrap it inside async
method, which will cancel Task
after some time.
Hope this helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With