【问题标题】:ArgumentOutOfRangeException on SerialPort.ReadTo()SerialPort.ReadTo() 上的 ArgumentOutOfRangeException
【发布时间】:2014-04-21 15:54:07
【问题描述】:

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

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;
            }
        });
}

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:

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

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 被分配一个负数,因为 CachedBytesToRead 是负数。内联 cmets 指定 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;
        }

有人对为什么会发生这种情况有任何合理的解释吗?
我不相信我在读/写/访问串行端口方面做任何非法的事情。
这被不断抛出,没有好的方法来重现它。
输入缓冲区上有可用的字节,在这里您可以看到一些关键属性在中断时的状态(readLen、readPos、BytesToRead、CachedBytesToRead):

我是不是做错了什么?


编辑:显示同一端口未从循环异步访问的图片:

【问题讨论】:

  • 我在使用串行端口方面的经验为零,所以请原谅我:您知道错误发生时您在流中的哪个位置吗?对我来说,您似乎在您的位置为 -1 且长度为零的流的开始或“结束”处遇到错误。也就是说,readPos 为零,长度为-1?这会导致 CachedBytesToRead 为负数。那可能对您没有帮助?
  • @rune711 你可以在最后一个截图中看到readPos 是 5,readLen 是 2。这仍然是 CachedBytesToRead 是 -3。你会认为如果CachedBytesToRead 小于bytesToRead 它会尝试从端口读取另一个块。
  • readLen 和 readPos 是 mscorlib.dll 中 SerialPort 实现的私有成员。我无法控制他们。我包括了关于它们的简介,因为它说明了为什么抛出异常,而不是 为什么 它正在发生,我不知道。我所做的只是在向端口写入一些数据后在端口上调用 ReadTo(NewLine) 以捕获它的响应。我事先知道数据是如何终止的。
  • TerminationCharacters 之间需要多少数据?您可能会溢出某个大小的内部整数。
  • ports 是否有可能多次包含同一个端口,因此您的 Parallel.ForEach 从同一个端口并行读取?

标签: c# .net winforms serial-port parallel.foreach


【解决方案1】:

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

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

简单的解释是您的List&lt;SuperSerialPort&gt; 集合多次包含同一个端口。 Parallel.ForEach() 语句触发比赛。工作一段时间就好了,直到两个线程同时执行decoder.GetChars()方法并且都到达下一条语句:

   readPos += bytesToRead;

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

#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

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

【讨论】:

  • 我将端口列表复制到了一个静态对象,这样当代码在并行循环中中断时,我可以检查内容。没有重用端口的指示,也没有从您的代码生成的异常。请参阅我的编辑以获取显示我的意思的图片。这是一种无效的测试方式吗?图片没有显示,但我确实在调试配置中。
  • 接下来要考虑的是 RetrieveCOMReadings() 被多个线程调用。添加Debug.WriteLine(System.Threading.Thread.CurrentThread.ManagedThreadId);
  • 不完全不相关,这是我之前问过的一个问题,这是异步访问串行端口的动机。在 UI 线程上读取多个串行端口正在扼杀响应能力。有没有更好的办法? stackoverflow.com/questions/19056306/…
  • 会试一试的。
  • 线程 ID 不同,但经常重复。我将 RetrieveCOMReadings() 作为一个单独的任务生成,并以某个可变的间隔调用它。这可能是我的问题吗?我想知道当下一个电话进来时 ReadTo 是否超时。
【解决方案2】:

这可能是因为您正在设置终止字符并将此字符用于 readto。而是尝试使用 ReadLine 或删除终止字符。

【讨论】:

  • 讽刺的是,ReadLine 函数是一个单行函数,定义为:return ReadTo(NewLine); 想知道 NewLine 的定义吗?:private const string defaultNewLine = "\n"; referencesource.microsoft.com/#System/sys/system/io/ports/…
  • 好的,那么我唯一能告诉你的是使用二进制异步事件,我经常使用带有 c# 的串行端口与微控制器通信,并且使用二进制事件/函数从来没有问题,但处理起来当然更复杂。
  • 我很欣赏输入,但是,并不是每个数据流都由 NewLine 终止。我不确定我是否遵循您的第二个建议;什么是二进制异步事件?你的意思是管理我自己的 SerialStreams 吗?
  • 挂钩到 datareceived 事件,然后在读取 BytesToRead(串行端口的属性)字节中使用 Read 函数将其存储到缓冲区中并进行处理
猜你喜欢
  • 1970-01-01
  • 2011-05-13
  • 1970-01-01
  • 2021-09-09
  • 2011-04-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多