C# waiting for input from another thread using AutoResetEvent

1.7k views Asked by At

I spent some time searching for an answer to this and found plenty of helpful information in other threads. I believe I've written the code in a way that works, but I am not happy with the outcome.

I designed a piece of hardware that I am communicating with via C#. The hardware connects via USB and runs initialization routines after enumerating with the OS. At that point, it simply waits for the C# program to start sending commands. In my C# code, the user must press a "Connect" button, which sends a command and the required payload to let the hardware know it should continue running. The hardware then sends a command back as an ACK. The problem is that my C# program must wait to receive the ACK, but the GUI is totally frozen until the hardware responds as I don't know how to partition it out to another thread that can block freely. If the hardware responds immediately, then it works fine, but if it can't connect, then the program stays frozen indefinitely.

With that said, I know a few things need to happen, but I'm not sure how to implement them. First and foremost, I don't think sitting in a loop waiting on a boolean is the right way to go, but using AutoResetEvent doesn't really seem to be much better. There has to be a better way involving timers, more threads, or something similar.

I am using the DataReceived event with the serialPort object as follows:

private void serialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
    byte cmd = (byte)serialPort1.ReadByte();

    if (cmd == (byte)Commands.USB_UART_CMD_MCU_CONNECT)
        MCU_Connect_Received.Set();

}

In the buttonClick function ("main" thread), the program stops while it waits for the ACK:

//Send the command to signal a connection
Send_Connection_Packet((byte)Commands.USB_UART_CMD_PC_CONNECT);

textBox1.AppendText("-I- Attempting to contact hardware...");

MCU_Connect_Received.WaitOne();

textBox1.AppendText("Success!" + Environment.NewLine);

Ideally, I'd like to know if a timeout expired so I can print "Failed!" instead of "Success!". Not having a timeout also means it will sit there forever, as I mentioned above, until I kill the process. It's possible that it won't find any hardware, but if it does, it should respond in < 1 second, so a timeout of 2 seconds would be more than enough. I tried using Thread.Sleep, but that froze the GUI as well.

4

There are 4 answers

2
Stephen Cleary On BEST ANSWER

I recommend you use the Task class. You can use a TaskCompletionSource to complete the task when the operation completes.

Using the new async support, your code then becomes:

textBox1.AppendText("-I- Attempting to contact hardware...");
await Send_Connection_Packet((byte)Commands.USB_UART_CMD_PC_CONNECT);
textBox1.AppendText("Success!" + Environment.NewLine); 

If you don't want to use the Async CTP, then you can call Task.ContinueWith and pass TaskScheduler.FromCurrentSynchronizationContext to schedule the textBox1.AppendText("Success!") line to run on the UI thread.

The async support also includes timers (TaskEx.Delay) and combinators (TaskEx.WhenAny), so you can easily check for timeouts:

textBox1.AppendText("-I- Attempting to contact hardware...");
var commTask = Send_Connection_Packet((byte)Commands.USB_UART_CMD_PC_CONNECT);
var timeoutTask = TaskEx.Delay(1000);
var completedTask = TaskEx.WhenAny(commTask, timeoutTask);
if (completedTask == commTask)
  textBox1.AppendText("Success!" + Environment.NewLine); 
else
  textBox1.AppendText("Timeout :(" + Environment.NewLine);
4
Robert Allan Hennigan Leahy On

The issue with the GUI freezing is because all the callbacks for GUI events occur in the thread that's running the GUI. If you don't want the GUI to freeze you need to spawn a new thread.

For implementing the timeout, you can do a timed wait on an event handle and then check the return value for true or false to determine if the call was successful or if it timed out.

2
diggingforfire On

If you want the GUI to remain responsive, you should run things in a background thread. A BackgroundWorker does this nicely. I'd stick with the resetevent over a busy wait construction. You can use a timer to trigger the resetevent after a timeout period

1
alexm On

To enable timeouts use another overload of WaitOne():

 bool succeeded = MCU_Connect_Received.WaitOne(timeOutInMilliseconds, false);
 if (succeeded)  
 {
       textBox1.AppendText("Success!" + Environment.NewLine);         
 }
 else
 {
       textBox1.AppendText("Failed!" + Environment.NewLine);         
 }

Consider moving communication-related code in a separate class to encapsulate the communication protocol. This way the code will be easier to maintain and you will able to implement all Task/background worker ideas the other people suggested.