【问题标题】:C# multiple serial ports simultanious readingC#多个串口同时读取
【发布时间】:2023-04-03 00:29:01
【问题描述】:

再次返回另一个串行端口问题。

在过去的两周里,我一直在优化我的代码以尝试读取两个串行设备。目前,该程序获得了几乎所有数据(大约 99% 与我之前的 50% 相比),但我仍然缺少数据。

这是我的“主要”功能:

private Program()
    {
        port.DataReceived += new SerialDataReceivedEventHandler(port_DataReceived);
        port2.DataReceived += new SerialDataReceivedEventHandler(port2_DataReceived);


        port.Open();
        port2.Open();


        Application.Run();
    }

这是我用于两个串行端口的代码:

public void port1IntoBuffer()
    {
        int messageLength = 96; 
        byte[] buffer = new byte[messageLength];
        port.BaseStream.ReadAsync(buffer, 0, messageLength);

        for (int i = 0; i < messageLength; i++)
        {
            if ((int)buffer[i] <= 48 && (int)buffer[i] > 0)
            {
                tickQ.Enqueue((new IdDate { Id = (int)buffer[i], Date = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") }));
            }
        }
        if (!Locked)
        {
            Locked = true;
            Thread readThread = new Thread(() => submitSQL());
            readThread.Start();
        }
    }

我也为端口 2 复制了这段代码。 messageLength 只是用于测试的任意数字。至于我的输入,我期待一个介于 1-48 之间的整数值。 tickQ 变量是一个 ConcurrentQueue,我将两个端口都排入队列(稍后将其出列并发送到 SQL DB)。

如果有人能给我一些关于我做错了什么的提示,我将不胜感激。

谢谢!

编辑1:

阅读BlueStrat的建议后,我将我的sql提交代码更改为以下:

public void submitSQL()
    {
        lock (Locked)
        {
            int retryCount = 3;
            bool success = false;
            while (retryCount > 0 && !success)
            {
                try
                {



                    IdDate tempData;
                    using (SqlConnection con = new SqlConnection())
                    using (SqlCommand cmd = new SqlCommand())
                    {

                        con.ConnectionString = "Data Source=xxxxxx;" +
                                                "Initial Catalog=xxxxxx;" +
                                                "User id=xxxxxx; Password=xxxxxx; Connection Timeout=0";

                        cmd.CommandText = "INSERT INTO XXXXX (submitTime, machineID) VALUES (@submitTIME, @machineId)";

                        cmd.Connection = con;


                        con.Open();
                        while (tickQ.TryDequeue(out tempData))
                        {


                            cmd.Parameters.Clear();
                            cmd.Parameters.AddWithValue("@machineId", (tempData.Id));
                            cmd.Parameters.AddWithValue("@submitTIME", tempData.Date);

                            cmd.ExecuteNonQuery();
                        }
                        con.Close();



                    }
                }
                catch (SqlException e)
                {
                    Console.WriteLine(e.Message);
                    retryCount--;
                }
                finally
                {
                    success = true;
                }
            }
        }
    }

现在我的问题是 10 分钟后,我的 RAM 使用量激增(从大约 6MB 增长到 150MB),然后程序崩溃。

编辑 2:在大家的建议下,结果有所改善!昨晚我只错过了大约 15,000 次传输中的 8 次。我将尝试增加我的串行读取缓冲区以希望捕获更多传输。

【问题讨论】:

  • 您可以详细说明预期的数据是什么以及您目前得到的数据是什么?
  • 我的预期数据是一个介于 1-48 之间的整数值,我记录了收到该值的时间(使用我的 DateTime.Now())。一旦我有两个相同整数值的数据点,我减去到达时间以获得持续时间。我的问题是偶尔会错过过渡,使我的持续时间比应有的长得多。
  • 我正在将我的结果与现有系统进行比较(我正在编写的程序将替换它)。我的程序通常读取 5 或 6 次(意思是,旧程序读取 500 次,但我的程序只读取 495 次)。我会尝试从这个旧的 VB 程序中复制代码,但它使用的是 MSCommLib,我真的不想使用它。
  • 这可能是,我会尝试缩小我的缓冲区大小。令人讨厌的是,我通常要等几个小时后才能判断我所做的更改是否有效(因为丢失的数据是如此不一致和罕见)。可悲的是,这个串行设备只传输数字,没有分隔字符或任何东西。
  • 啊,这是个好主意。我将尝试进行测试。感谢您迄今为止的帮助!

标签: c# multithreading serial-port


【解决方案1】:

我在您的代码中注意到了几个问题,第一个问题是我作为评论解决的。 'ReadAsync()' 函数调用是异步的,这意味着它会提前返回,同时在后台获取请求的数据。

通常,您可以通过在返回的Task 上发出await 来处理此问题,表明您希望代码在请求的字节数被完全读取后恢复(不阻塞当前线程)。

await 仅从 C# 5 开始可用。如果您在较低版本上运行,则通常会安排一个延续,这是一种在任务完成后运行的委托。不过,我认为简单地切换到Read() 版本就可以了,线程在等待数据时肯定会阻塞,但至少你确定你已经读取了预期的字节数。

我注意到的另一个问题是您的锁定代码依赖于布尔标志,它不是线程安全的。问题在于检查Lock 标志状态以有条件地进入受保护块的代码以及实际设置块内标志状态的代码不是原子的。 这意味着一个不同的线程可以进入你试图保护的代码,而另一个线程也进入了相同的代码块,但仍然没有设法将保护标志切换为 true,在这种情况下,两个线程将运行相同的代码并可能产生意外的结果(不确定这种可能性是否与您丢失数据的问题有关)。

如果不查看Lock 成员的其他用途以及它是如何释放的,我真的无法推荐如何重写您的锁定保护代码。如果您发布与Lock 成员交互的其余代码 sn-ps,我也许可以提供一些建议。

编辑:(OP 评论后的更多信息)

该标志的目的是防止每个线程发布到 一次数据库(我不断收到死锁错误,但我没有 认为这会导致我的数据丢失)。

有了这些信息,我认为您可能还有另一个问题可以解释丢失的数据。如果 Flag 为真,会发生什么?您只需跳过启动向您的 SQL 服务器提交数据的线程,您永远不会重试,因此一旦方法退出,缓冲的数据就会丢失抱歉,您不会丢失数据,因为您正在排队,您可能会失去实际耗尽数据接收队列所需的线程启动量。当您遇到数据丢失时,请检查您的tickQ 队列,我敢打赌它仍然有数据要处理,因为同样的原因,应该处理它的线程从未启动过。

submitSQL() 函数内添加lock() 语句将保护您免受这种可能性的影响。

从您的代码中完全删除 Locked 成员并尝试以下操作:

// Add this member to your class;
object SqlLock = new object();



    void submitSQL() {
      // add this at the start of your submitSQL() method
       lock (SqlLock ) {
         ... the rest of your code
       }
    }

与您当前代码的不同之处在于,如果 SQL 实例正在被另一个线程使用,调用线程将在继续之前阻塞并等待它可用,而不是简单地 丢弃刚刚读取的数据。 不启动处理排队数据的线程。

编辑:最后一条建议

我建议您不要在每次需要排空队列时都创建新线程,而是可以依赖线程池(请参阅 Task.Run())。使用池中的线程消除了手动创建线程的昂贵开销。

另一种可能性是创建一个单独的线程,该线程将持续专门用于排空数据队列。您可以使用AutoResetEvent 来协调生产者线程(入队的线程)和消费者线程(出队的线程)之间的工作 您的消费者线程进入一个调用AutoResetEvent.WaitOne() 的循环,它会阻塞;您的生产者线程将接收到的数据排入队列,而不是生成一个新线程,而是调用AutoResetEvent.Set() 导致消费者线程唤醒并处理排队的数据,一旦处理完毕,它会再次阻塞,等待下一批数据到达.如果这样做,请确保将您的线程标记为BackgroundThread,这样就不会阻止应用程序在线程等待数据时关闭。

【讨论】:

  • 感谢您指出我的非线程安全布尔标志,我什至没有想到这不是线程安全的。该标志的目的是防止每个线程同时发布到数据库(我不断收到死锁错误,但我认为这不会导致我的数据丢失)。我也确实从 asyncread 更改为 read(AsyncRead 开始变慢很多,数据需要 10 多分钟才能到达数据库)。
  • 感谢 BlueStart 的所有帮助,我已从布尔值更改为锁定。如果您有时间,请参考我在 OP 中的编辑,其中包含我的 submitSQL 函数的更新版本。
  • 您的submitSQL() 函数中存在一个小问题。 success=true; 语句不应在您的 finally 块内。它应该在con.Close() 之后。否则,您将永远不会重试,因为即使您捕获了异常,finally 块(表示成功的地方)也会运行。
  • 查看您的完整 submitSQL() 实现,我认为如果您碰巧收到最后一批数据,就在排空队列的 while() 块退出之后,您经历的数据丢失,但是在您清除 Locked 标志之前,这将导致您的原始代码永远不会安排耗尽最终数据的线程。希望这一切都有帮助!
  • 非常感谢 BlueStrat!我会让这些更改通宵运行,看看它是否有助于数据丢失。手指交叉!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-12-05
  • 1970-01-01
  • 2021-07-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多