1

I'm experiencing a weird behavior while trying to stop a SerialPort: the DataReceived event continues to fire after unsubscribing and after calling close! (see StopStreaming in the following code). As a result, in my event handler code I get an InvalidOperationException with the message that "The port is closed".

What am I missing? What is the correct way to close the port and stop the events?

EDIT: I get this error every time I run my code. So this is not a race condition that happens randomly but rather a systematic problem indicating a completely broken code! However, I fail to see how...

private SerialPort comPort = new SerialPort();

public override void StartStreaming()
{
  comPort.Open();
  comPort.DiscardInBuffer();
  comPort.DataReceived += comPort_DataReceived;
}

public override void StopStreaming()
{
  comPort.DataReceived -= comPort_DataReceived;
  comPort.Close();
  isStreaming = false;
}

private void comPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
  if (e.EventType == SerialData.Chars)
  {
    SerialPort port = (SerialPort)sender;
    int N = comPort.BytesToRead;
    for (; N > 0; N--)
    {
      byte b = Convert.ToByte(comPort.ReadByte());
      //... process b
    }
  }
}

EDIT: following the suggestions, I changed StopStreaming code to something like this:

public override void StopStreaming()
{
  comPort.DataReceived -= comPort_DataReceived;
  Thread.Sleep(1000);
  comPort.DiscardInBuffer();
  Thread.Sleep(1000);
  comPort.Close();
  isStreaming = false;
}

It seems to work now but I'm not really that happy. I wish there was a more effective way to remove the callback rather than inserting sleep periods in the program.

AlefSin
  • 1,086
  • 9
  • 20
  • Have you tried a `DiscardInBuffer` when you close as well? – paul Dec 14 '12 at 10:01
  • Interesting idea. I tried it but didn't help :( I added it in `StopStreaming` right after the part that removes the event. – AlefSin Dec 14 '12 at 10:12

4 Answers4

6

Your DataReceived event handler is called on a threadpool thread. And yes, they've got the awkward habit of running your code at an unpredictable time, it is not instant. So it is fairly inevitable that, if the device is actively sending data, that it can race with your Close() call and run after you closed it. Unsubscribing doesn't fix it, the threadpool thread already got its target method.

Do realize what you are doing to trigger this problem, you are closing the port while the device is sending data. That's not great, it is guaranteed to cause data loss. But not unlikely to happen when you are debugging your code since you don't actually care about the data.

A counter-measure is to turn off handshaking so the device cannot send anything anymore. Discard the input buffer. Then sleep for a while, a second or two, to ensure that any threadpool threads in-flight have completed running. Then close the port. A very pragmatic one is to simply not close the port, Windows will take care of it when your process terminates.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Thanks for your answer. I prefer to close the port somehow. This is in fact some kind of simple port monitor so it is true that the communication is going to be cut in the middle of transmission. The data loss, however, is not that important. I'd like to find a robust solution to connect/disconnect as many time I like during the lift time of the process. – AlefSin Dec 14 '12 at 13:08
  • Well, I documented one. Before "pragmatic". You ought to try it. – Hans Passant Dec 14 '12 at 13:17
  • I tried it and seems to work for now. I'm not happy though as it looks like a temporary hack rather than a robust solution. I'm going to accept your answer though. Thanks. Since the is in fact a virtual comport provided by the FTDI chip, I'm going to try completely different approach next based on their D2XX API that does not need any SerialPort nonesense. – AlefSin Dec 14 '12 at 13:32
  • Interestingly, it looks like the devs went to a lot of effort to prevent this exact situation, as seen from [SerialPort.cs](https://referencesource.microsoft.com/#System/sys/system/io/ports/SerialPort.cs,1430) and [SerialStream.cs](https://referencesource.microsoft.com/#System/sys/system/io/ports/SerialStream.cs,848). Unfortunately, it looks like the performance tradeoff was not worth it, as I can confirm I can still get this issue when concurrently closing the serial port as of 2020. – glopes Aug 26 '20 at 10:20
2

Looks like multi-threading issue.

SerialPort.DataReceived is raised in a worker thread from thread pool. You're closing port from the thread, that differs from the thread, where SerialPort.DataReceived raised in.

You can handle InvalidOperationException or write some synchronization code to solve this problem.

Update.

The problem is that if your devices sends data intensively, SerialPort queues more and more work items to thread pool. Even if your code will sleep any time before Close, it can be not enough. Unfortunately, SerialPort has an ugly implementation. It hasn't an option, which could tell "please, stop spam me with your data".

Hence, the concrete solution depends on device's protocol and handshaking parameters.

Dennis
  • 37,026
  • 10
  • 82
  • 150
  • Well, I assume the call back runs in a thread that otherwise I have no access to. I don't see a clear, bullet proof way to avoid race conditions while at the same time guarantee that the callback would not get blocked or miss data. Do you have a suggestion how to practically do that? – AlefSin Dec 14 '12 at 13:13
  • @AlefSin: there isn't such clear way. I've updated the answer. – Dennis Dec 14 '12 at 13:26
  • 1
    Thanks for your time and your input. The two answers were very close and I could only accept only one of them. – AlefSin Dec 14 '12 at 14:54
0

I had the same problem in an application I've been working on. It's exciting to read here about how the threadpool can bring it about.

Before I tracked down it's source though, I found that enclosing the contents of the DataReceived event handler in a try catch statement written in anticipation of the problem was a very effective way to solve it. Now that I know there's not really anything I can do to prevent the issue if I need/want to close a SerialPort while still receiving data, I'm quite happy with this approach.

0

I had similar issue when the user attempted to Exit application whilst it was still receiving data from the connected device. Application was throwing a System.IO.IOException following call to Me.Close().

Simplest solution I found was to set the SerialPort ReceivedBytesThreshold to a large number in the _FormClosing event handler. This reduces the frequency of DataReceived events and provides time for the Close() call to complete whilst the DataReceived event handler is inactive.