1

Context: I'm writing a .NET library that communicates with a POS device over a serial port. I have to send specific commands over the serial port and then wait for a maximum amount of time for the device to answer back.

As I understand, the SerialPort object in .NET uses an event handler to signal when bytes are returned over the wire and this event handler is executed on a separate thread.

My plan is to use one main thread to manage the communication with the device, waiting for bytes to be received, parsing, responding, etc, and have the event handler for SerialPort.DataReceived somehow "signal" back to the thread on which the communication is being managed, when that thread has to wait for bytes from the device. A CancellationToken might also be involved on the main thread, in case the user decides to cancel the operation.

So I need:

  1. to make one thread (the main communication logic one) wait...
  2. ...until either a second thread (the one on which the DataReceived event handler fires) does some work or until a timeout occurs.

Initially I tried using Monitor.Pulse and Monitor.Wait for this, but I realized that Monitor.Wait actually never returns if another thread does not release the lock, so I can end up with my main thread waiting forever (let's say the communication fails, etc).

After that, I started looking at EventWaitHandle for this, but I'm not sure if it's the right choice. My plan now is as follows:

  1. Create a new EventWaitHandle(false, EventResetMode.AutoReset)
  2. When waiting for bytes, on the main thread, do a WaitHandle.WaitAny(new WaitHandle[] { _eventWaitHandle }, timeoutMilliseconds);
  3. When bytes are received, on the event handler thread, do a _eventWaitHandle.Set();

This should also handle, I think, the case when, between sending some bytes and starting to wait for bytes to be received, some bytes are already received on the second thread as, if I understand correctly, WaitHandle.WaitAny returns successfully right away if the EventWaitHandle instance is already signaled.

I'd really like to know if I'm making any incorrect assumptions here about my plan, as multi-threaded programming is really not something I'm knowledgeable about.

EDIT: Since I realized that it was not initially clear, what I'm really asking is whether using EventWaitHandle and WaitHandle.WaitAny is a correct way to have one thread wait for a second thread to signal to it that it has done some work (within a specified timeframe), and then the waiting thread can continue its work.

LATER EDIT: Specifically for SerialPort, I did look at avoiding having to work with thread synchronization and using async/await via SerialPort.BaseStream, but seems like that basically would involve reimplementing what's already available via the provided events, especially since ReadAsync does not really cancel on timeout via a CancellationToken unless the passed token is already canceled, (found this on other related questions and verified myself) which kinda defeats the purpose of using it at all, for me.

I ended up deciding to wrap my "control" thread in some async/await code and then synchronizing it with the thread created by SerialPort via AutoResetEvent, since there's just two threads that need to be synchronized and it changes status automatically. Thanks for pointing out that it is avaialble!

cearny
  • 81
  • 4
  • 1
    If the code works, then a question asking if it's "the right way" or "the best way" or anything similar is primarily opinion based. That said, to the extent that an answer could even be provided, we already have questions with answer on the site that do answer that. See duplicates. IMHO, you should be using async/await with the serial port's `BaseStream` object, as recommended in the duplicates. You can further wrap the `BaseStream` in a `StreamReader` if you need a text/line-based view of the data from the serial port. Either way, use async/await. – Peter Duniho May 26 '21 at 16:38
  • @PeterDuniho thanks for pointing me to just using the underlying stream. The point is that I'm not really knowledgeable about multithreading primitives so I'm really just bashing them together in hopes that I'm understanding the documentation correctly. That's why I was wondering if it is the right classes that I'm using, and if I'm using them in the right way. – cearny May 26 '21 at 17:06
  • _"I was wondering if it is the right classes that I'm using, and if I'm using them in the right way"_ -- if it works, then to the best first approximation we can offer here, you are using them in the right way. It's my _opinion_ that there are better ways to do it, but that's not the kind of question and answer that Stack Overflow is designed for. – Peter Duniho May 26 '21 at 17:11
  • @PeterDuniho That's the point of me asking the question, though: am I using the right tools to wait for an event to fire on a different thread? It looks like it to me, but I'm no multithreading expert, I just know enough to know how much I don't know at all, if that makes any sense. I'm sorry if *that* didn't come across because I added too much specific context. – cearny May 26 '21 at 17:15
  • 1
    _"am I using the right tools to wait for an event to fire on a different thread?"_ -- that's a loaded question. In my _opinion_, using modern C# there's no reason for a thread to wait at all. If you do want to make a thread wait then, sure...`AutoResetEvent` is old-school but still works fine. But it's much better to just use async/await instead. – Peter Duniho May 26 '21 at 17:41

1 Answers1

1

You do not have to use the data received event. The serial port does expose regular synchronous read/write methods that might be appropriate to use in this case.

To make synchronous resources like this easier to use it can be a good idea to wrap them in an asynchronous interface. For example:

public class MySerial
{
    private SerialPort mySerialPort = new ();
    private BlockingCollection<(TaskCompletionSource<string> result, string data)> queue = new(new ConcurrentQueue<(TaskCompletionSource<string> result, string data)>());

    public MySerial() => Task.Run(ThreadMain);

    private void ThreadMain()
    {
        // loop will block if no messages are queued
        // call queue.CompleteAdding() to end the enumerable and exit the loop.
        foreach (var (tcs, input) in queue.GetConsumingEnumerable())
        {
            mySerialPort.WriteLine(input);
            var result = mySerialPort.ReadLine();
            tcs.SetResult(result);
        }
    }
    public Task<string> QueueWrite(string input)
    {
        var tcs = new TaskCompletionSource<string>();
        queue.Add((tcs, input));
        return tcs.Task;
    }
}

This uses one thread to manage all communication with the serial port, and uses a TaskCompletionSources to report the progress. Note that this has been significantly simplified to keep it short, some things to consider:

  • You should probably set read/write timeouts
  • you probably want to catch exceptions when reading/writing to the serial port, and forward any exceptions to the taskCompletionSource.
  • This needs to be disposable, but you need to consider the details, like if all queued messages should be processed first.
  • This will block a thread even if no messages are being processed. This might or might be an issue in your use case.
  • Cancellation support could be added.
  • It would be possible to use the same pattern while using the events, but it would be a bit more cumbersome.
  • this uses WriteLine/ReadLine, but the same pattern should be possible to use with any of the write/read methods.
JonasH
  • 28,608
  • 2
  • 10
  • 23
  • Much thanks for taking time to write the code outline and the bullet list! I realized meanwhile that because I added too much context, the point of my question got lost. My question isn't really about `SerialPort`, but rather about properly having one thread wait for another thread to finish its work, or otherwise timeout. And, yeah, I'm even more lost about how to wrap that in an `async/await` anyway, so I don't think I want to touch that, really. – cearny May 26 '21 at 17:24
  • @cearny To simply wait there is [ManualResetEvent(slim)](https://learn.microsoft.com/en-us/dotnet/api/system.threading.manualreseteventslim?view=net-5.0). This will however block the thread, so it is not very useful for UI programs. (never block main thread). instead you should send a message to the main thread to do whatever it needs to do after background work has completed. Tasks and async/await is probably the best way to do this. The older style was to pass a "callback" delegate that would be invoked after the work had completed. – JonasH May 26 '21 at 21:12
  • 1
    @cearny Also, as a general precaution, multi-threading is difficult. You should learn about the hazards and how to mitigate them. Otherwise it is easy to cause bugs that can be very difficult to reproduce. – JonasH May 26 '21 at 21:16
  • thanks for putting things into perspective re multithreaded programming. I wish I could write the code in a blocking manner but indeed that is not how the world really works in this situation (device might timeout while waiting for an answer or communicate in bursts that need to be stitched together, user might want to abort, all that fun stuff). I guess I'll make sure I can limit the design to one control thread besides the UI one and then I can get away with `AutoResetEvent` even, since that one resets automatically and wrap that thread in an async/await-exposing object. – cearny May 27 '21 at 08:04