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:
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):
Am I doing something glaringly wrong?
EDIT: A picture showing that the same port isn't being asynchronously accessed from the loop:
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.
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