如何改进这个性能不佳、糟糕的串行端口代码?

Posted

技术标签:

【中文标题】如何改进这个性能不佳、糟糕的串行端口代码?【英文标题】:How can I improve this underperforming, terrible Serial Port code? 【发布时间】:2009-08-05 15:20:23 【问题描述】:

我有一段丑陋的串口代码,非常不稳定。

void port_DataReceived(object sender, SerialDataReceivedEventArgs e)

    Thread.Sleep(100);
    while (port.BytesToRead > 0)
    
        var count = port.BytesToRead;

        byte[] buffer = new byte[count];

        var read = port.Read(buffer, 0, count);

        if (DataEncapsulator != null)
            buffer = DataEncapsulator.UnWrap(buffer);


       var response = dataCollector.Collect(buffer);

       if (response != null)
       
           this.OnDataReceived(response);
       

       Thread.Sleep(100);
        

如果我删除任何一个 Thread.Sleep(100) 调用,代码将停止工作。

当然,这确实会减慢速度,如果大量数据流入, 除非我让睡眠变得更大,否则它也会停止工作。 (像纯粹的死锁一样停止工作)

请注意 DataEncapsulator 和 DataCollector 是组件 由MEF提供,但它们的性能相当不错。

该类有一个 Listen() 方法,该方法启动一个后台工作者 接收数据。

public void Listen(IDataCollector dataCollector)

    this.dataCollector = dataCollector;
    BackgroundWorker worker = new BackgroundWorker();

    worker.DoWork += new DoWorkEventHandler(worker_DoWork);
    worker.RunWorkerAsync();


void worker_DoWork(object sender, DoWorkEventArgs e)

    port = new SerialPort();

    //Event handlers
    port.ReceivedBytesThreshold = 15;
    port.DataReceived += new SerialDataReceivedEventHandler(port_DataReceived);

    ..... remainder of code ...

欢迎提出建议!

更新: *只是简要说明 IDataCollector 类的作用。 无法知道是否已发送数据的所有字节 在单个读取操作中读取。所以每次读取数据时 传递给 DataCollector,它在完成时返回 true 并且 已收到有效的协议消息。在这种情况下,它只是 检查同步字节、长度、CRC 和尾字节。真正的工作 稍后由其他类完成。 *

更新 2: 我现在按照建议替换了代码,但仍然有问题:

void port_DataReceived(object sender, SerialDataReceivedEventArgs e)

        var count = port.BytesToRead;

        byte[] buffer = new byte[count];

        var read = port.Read(buffer, 0, count);

        if (DataEncapsulator != null)
            buffer = DataEncapsulator.UnWrap(buffer);

        var response = dataCollector.Collect(buffer);

        if (response != null)
        
            this.OnDataReceived(response);
             

您会发现,这在连接快速稳定的情况下效果很好。 但是每次接收到数据时都不会调用 OnDataReceived。 (有关更多信息,请参阅 MSDN 文档)。所以如果数据变得碎片化 并且您只在事件数据丢失时读取一次。

现在我记得为什么我首先要有循环,因为 如果连接速度慢或不稳定,它实际上必须读取多次。

显然我无法回到while循环解决方案,那我该怎么办?

【问题讨论】:

把它拖到棚子后面开枪! ;-) 【参考方案1】:

我对基于 while 的原始代码片段的第一个关注点是为字节缓冲区不断分配内存。在此处放置一个“新”语句,专门针对 .NET 内存管理器为缓冲区分配内存,同时获取在最后一次迭代中分配的内存并将其发送回未使用的池中以进行最终的垃圾回收。在一个相对紧凑的循环中,这似乎是一项非常艰巨的工作。

我很好奇通过在设计时以合理的大小(例如 8K)创建此缓冲区可以获得的性能改进,因此您不需要所有这些内存分配、释放和碎片。会有帮助吗?

private byte[] buffer = new byte[8192];

void port_DataReceived(object sender, SerialDataReceivedEventArgs e)

    Thread.Sleep(100);
    while (port.BytesToRead > 0)
    
        var count = port.BytesToRead;

        var read = port.Read(buffer, 0, count);

        // ... more code   
    

我对在循环的每次迭代中重新分配此缓冲区的另一个担忧是,如果缓冲区已经足够大,则可能不需要重新分配。考虑以下几点:

循环迭代 1:收到 100 个字节;分配 100 字节的缓冲区 循环迭代 2:收到 75 个字节;分配 75 字节的缓冲区

在这种情况下,您实际上不需要重新分配缓冲区,因为在循环迭代 1 中分配的 100 字节的缓冲区足以处理在循环迭代 2 中接收到的 75 字节。不需要销毁 100 字节缓冲区并创建一个 75 字节缓冲区。 (当然,如果您只是静态地创建缓冲区并将其完全移出循环,那么这是没有实际意义的。)

另一方面,我可能建议 DataReceived 循环只关注数据的接收。我不确定那些 MEF 组件在做什么,但我怀疑它们的工作是否必须在数据接收循环中完成。是否可以将接收到的数据放在某种队列中,并且 MEF 组件可以在那里提取它们?我对尽可能快地保持 DataReceived 循环感兴趣。也许接收到的数据可以放在一个队列中,以便它可以立即返回工作接收更多数据。您可能可以设置另一个线程来监视到达队列的数据,并让 MEF 组件从那里获取数据并从那里开始工作。这可能需要更多的编码,但它可能有助于数据接收循环尽可能地响应。

【讨论】:

【参考方案2】:

而且可以这么简单……

您要么使用 DataReceived 处理程序,但没有循环,当然也没有 Sleep(),读取准备好的数据并将其推送到某处(到队列或 MemoryStream),

启动一个线程(BgWorker)并执行(阻塞)serialPort1.Read(...),然后再次推送或组装您获得的数据。

编辑:

根据您发布的内容,我想说:删除事件处理程序,然后读取 Dowork() 中的字节。这样做的好处是您可以指定所需的数据量,只要它(很多)小于 ReadBufferSize。

Edit2,关于 Update2:

在 BgWorker 中使用 while 循环仍然会好得多,根本不使用事件。简单的方法:

byte[] buffer = new byte[128];  // 128 = (average) size of a record
while(port.IsOpen && ! worker.CancelationPending)

   int count = port.Read(buffer, 0, 128);
   // proccess count bytes


现在,您的记录可能是可变大小的,您不想等待接下来的 126 个字节进入以完成一个。您可以通过减小缓冲区大小或设置 ReadTimeOut 来调整它。要获得非常细粒度,您可以使用 port.ReadByte()。由于它是从 ReadBuffer 中读取的,因此它并没有真正变慢。

【讨论】:

嗨,这就是 DataCollector 所做的。 DataCollector 收集所有字节,直到它有一个完整的协议消息。每次调用 collect 时,它都会返回 false,直到它有完整的协议消息。我会将其添加到原始问题中。我得先看看你的建议 我已经在编辑了。我建议在 BgWorker 中进行阻塞读取。 我想你已经解决了我的问题 :) 我会做更多的测试,但现在你的答案是正确的 :D 不适用于慢速连接(或不稳定的连接)。请参阅有关 .BytesToRead 以及如何不为每个字节调用 OnDataReceived 的 MSDN 文档 我不明白问题出在哪里,也不知道 BytesToRead 与它有什么关系。或 OnDataReceived。你也不需要。【参考方案3】:

如果您想将数据写入文件并且串行端口经常停止,这是一种简单的方法。如果可能的话,使您的缓冲区足够大,以容纳您计划放入单个文件中的所有字节。然后在您的 datareceived 事件处理程序中编写代码,如下所示。然后,当您获得机会时,将整个缓冲区写入文件,如下所示。如果您必须在串行端口读取缓冲区时从缓冲区读取,请尝试使用缓冲流对象以避免死锁和竞争条件。

private byte[] buffer = new byte[8192]; 
var index = 0;
void port_DataReceived(object sender, SerialDataReceivedEventArgs e)
    
    index += port.Read(buffer, index, port.BytesToRead);
 

void WriteDataToFile()

    binaryWriter.Write(buffer, 0, index); 
    index = 0;

【讨论】:

以上是关于如何改进这个性能不佳、糟糕的串行端口代码?的主要内容,如果未能解决你的问题,请参考以下文章

GoogLeNet(含并行连接的网络)

RS-485 串行端口波特率性能效率

使用 Qt GUI 从所有可用的串行端口中进行选择

如何在 Linux 中查找并停止锁定串行端口的内容

如何正确使用 COMMTIMEOUTS 和从串行端口读取的 OVERLAPPED IO 模式

如何通过usb端口通信从arduino将串行数据写入phpmyadmin