Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ArgumentOutOfRangeException on SerialPort.ReadTo()

My code indeterminately throws ArgumentOutOfRangeException: Non-negative number required. when invoking the ReadTo() method of the SerialPort class:

public static void RetrieveCOMReadings(List<SuperSerialPort> ports)
{
    Parallel.ForEach(ports, 
        port => port.Write(port.ReadCommand));

    Parallel.ForEach(ports,
        port =>
        {
            try
            {
                // this is the offending line.
                string readto = port.ReadTo(port.TerminationCharacter);

                port.ResponseData = port.DataToMatch.Match(readto).Value;
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
                port.ResponseData = null;
            }
        });
}

SuperSerialPort is an extension of the SerialPort class, primarily to hold information required for communications specific to each device on the port.
A port always has the TerminationCharacter defined;
Most of the time it's a newline character:

enter image description here

I don't understand why this is happening.
If the ReadTo fails to find the character(s) specified in the input buffer, shouldn't it just timeout and return nothing?


The StackTrace is pointing to an offending function in the mscorlib, in the definition of the SerialPort class:

System.ArgumentOutOfRangeException occurred
  HResult=-2146233086
  Message=Non-negative number required.
Parameter name: byteCount
  Source=mscorlib
  ParamName=byteCount
  StackTrace:
       at System.Text.ASCIIEncoding.GetMaxCharCount(Int32 byteCount)
  InnerException:

I followed it and here's what I found:

private int ReadBufferIntoChars(char[] buffer, int offset, int count, bool countMultiByteCharsAsOne)
{
    Debug.Assert(count != 0, "Count should never be zero.  We will probably see bugs further down if count is 0.");

    int bytesToRead = Math.Min(count, CachedBytesToRead);

    // There are lots of checks to determine if this really is a single byte encoding with no
    // funky fallbacks that would make it not single byte
    DecoderReplacementFallback fallback = encoding.DecoderFallback as DecoderReplacementFallback;
    ----> THIS LINE 
    if (encoding.IsSingleByte && encoding.GetMaxCharCount(bytesToRead) == bytesToRead && 
        fallback != null && fallback.MaxCharCount == 1)
    {   
        // kill ASCII/ANSI encoding easily.
        // read at least one and at most *count* characters
        decoder.GetChars(inBuffer, readPos, bytesToRead, buffer, offset); 

bytesToRead is getting assigned a negative number because CachedBytesToRead is negative. The inline comments specify that CachedBytesToRead can never be negative, yet it's clearly the case:

    private int readPos = 0;    // position of next byte to read in the read buffer.  readPos <= readLen
    private int readLen = 0;    // position of first unreadable byte => CachedBytesToRead is the number of readable bytes left.

    private int CachedBytesToRead {
        get {
            return readLen - readPos;
        }

Anyone have any rational explanation for why this is happening?
I don't believe I'm doing anything illegal in terms of reading/writing/accessing the SerialPorts.
This gets thrown constantly, with no good way to reproduce it.
There's bytes available on the input buffer, here you can see the state of some of the key properties when it breaks (readLen, readPos, BytesToRead, CachedBytesToRead):

enter image description here

Am I doing something glaringly wrong?


EDIT: A picture showing that the same port isn't being asynchronously accessed from the loop: enter image description here

like image 484
B L Avatar asked Oct 20 '22 09:10

B L


1 Answers

This is technically possible, in general a common issue with .NET classes that are not thread-safe. The SerialPort class is not, there's no practical case where it needs to be thread-safe.

The rough diagnostic is that two separate threads are calling ReadTo() on the same SerialPort object concurrently. A standard threading race condition will occur in the code that updates the readPos variable. Both threads have copied the same data from the buffer and each increment readPos. In effect advancing readPos too far by double the amount. Kaboom when the next call occurs with readPos larger than readLen, producing a negative value for the number of available bytes in the buffer.

The simple explanation is that your List<SuperSerialPort> collection contains the same port more than once. The Parallel.ForEach() statement triggers the race. Works just fine for a while, until two threads execute the decoder.GetChars() method simultaneously and both arrive at the next statement:

   readPos += bytesToRead;

Best way to test the hypothesis is to add code that ensures that the list does contain the same port more than once. Roughly:

#if DEBUG
        for (int ix = 0; ix < ports.Count - 1; ++ix)
            for (int jx = ix + 1; jx < ports.Count; ++jx)
                if (ports[ix].PortName == ports[jx].PortName)
                    throw new InvalidOperationException("Port used more than once");
#endif

A second explanation is that your method is being calling by more than one thread. That can't work, your method isn't thread-safe. Short from protecting it with a lock, making sure that only one thread ever calls it is the logical fix.

like image 144
Hans Passant Avatar answered Oct 23 '22 04:10

Hans Passant