SerialPort.ReadTo()上的ArgumentOutOfRangeException

B L*_*B L 5 .net c# serial-port winforms parallel.foreach

ArgumentOutOfRangeException: Non-negative number required.调用类的ReadTo()方法时,我的代码不确定地抛出SerialPort:

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;
            }
        });
}
Run Code Online (Sandbox Code Playgroud)

SuperSerialPortSerialPort类的扩展,主要用于保存端口上每个设备特定的通信所需的信息.
一个端口总是有TerminationCharacter定义的;
大部分时间它都是换行符:

在此输入图像描述

我不明白为什么会这样.
如果ReadTo无法找到输入缓冲区中指定的字符,那么它不应该只是超时并且不返回任何内容吗?


StackTrace指向mscorlib中的一个违规函数,在SerialPort类的定义中:

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:
Run Code Online (Sandbox Code Playgroud)

我跟着它,这就是我发现的:

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); 
Run Code Online (Sandbox Code Playgroud)

bytesToRead正被分配负数,因为CachedBytesToRead是负数.内联注释指出CachedBytesToRead永远不会是负面的,但显然是这样的:

    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;
        }
Run Code Online (Sandbox Code Playgroud)

任何人都有理由解释为什么会这样?
我不相信我在读取/写入/访问SerialPorts方面做了任何非法行为.
这会不断被抛出,没有好的方法来重现它.
输入缓冲区中有可用的字节,在这里您可以看到一些关键属性中断时的状态(readLen,readPos,BytesToRead,CachedBytesToRead):

在此输入图像描述

我做错了什么吗?


编辑:显示相同端口未从循环异步访问的图片: 在此输入图像描述

Han*_*ant 3

这在技术上是可行的,通常是非线程安全的 .NET 类的常见问题。SerialPort 类不是,没有实际情况需要它线程安全的。

粗略的诊断是两个单独的线程同时在同一个 SerialPort 对象上调用 ReadTo()。更新readPos变量的代码中将出现标准线程竞争条件。两个线程都从缓冲区复制了相同的数据,并且每个增量 readPos。实际上,将 readPos 提前了两倍。当下一次调用发生且 readPos 大于 readLen 时,会发生 Kaboom,从而为缓冲区中的可用字节数生成负值。

简单的解释是您的List<SuperSerialPort>集合多次包含相同的端口。Parallel.ForEach() 语句触发竞争。暂时工作得很好,直到两个线程同时执行 Decoder.GetChars() 方法并且都到达下一条语句:

   readPos += bytesToRead;
Run Code Online (Sandbox Code Playgroud)

测试假设的最佳方法是添加代码,确保列表多次包含相同的端口。大致:

#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
Run Code Online (Sandbox Code Playgroud)

第二种解释是您的方法正在被多个线程调用。那是行不通的,你的方法不是线程安全的。除了用锁保护它之外,确保只有一个线程调用它才是合乎逻辑的修复方法。