最近使用WPF开发串口助手时,遇到一个很奇怪的问题,无论是主线程、异步还是多线程,当串口接收速度达到0.016s一次以上,就会发生字符串缺失问题并且很卡。而0.016s就一切如常,仿佛0.015s与0.016s是天堑之隔。
同一份代码放在多线程(主线程或异步)环境中:由于是在测试,尝试过用索引下标的方式遍历数组
其中allData的定义为
csList<string> allData = new List<string>();
csif (allData.Count > 0) { RecvBuffer.Clear(); StringBuilder sb = new StringBuilder(); sb.Append(DateTime.Now); sb.Append(">>"); Application.Current.Dispatcher.Invoke(() => { for (int i = 0; i < allData.Count; i++) { RecvBuffer.Append(sb); RecvBuffer.Append(allData[i]); } TextEditor?.AppendText(RecvBuffer.ToString()); }); }
0.016s一次接收:
日期可以正常打印下来,而且打印的速度很快,整体显示很流畅
0.015s一次接收:
虽然串口发送脚本的速度只提高了一点点,但是日期时间几乎不打印,只有零星几个有,而且整个显示非常卡
按理来说这两个速度差别应该不大,不至于产生这种差别,后来又相继测试了多线程、异步、主线程的全速打印(直接放进while死循环里,拟定的固定字符串) ,但都可以正常显示日期时间。于是只能怀疑到字符串本身的问题了,由于此前本人是学C/C++的,对C#的字符串原理还不甚了解,大概知道C#的字符串中的"\0"并非作为字符串的结尾。但此时的打印结果却让我对C#的字符串拼接原理产生了深深的困惑。
根据前面的全速打印等测试情况,如果把硬编码的字符串或者数字塞进StringBuilder是没有问题的,所以想知道是不是字符串本身的问题,于是我把allData里的数据先转为数字,再直接拼接字符串:
csif (allData.Count > 0) { RecvBuffer.Clear(); StringBuilder sb = new StringBuilder(); sb.Append(DateTime.Now); sb.Append(">>"); Application.Current.Dispatcher.Invoke(() => { for (int i = 0; i < allData.Count; i++) { RecvBuffer.Append(sb); var value = float.TryParse(allData[i], out var val); RecvBuffer.Append($"{val}\n"); } TextEditor?.AppendText(RecvBuffer.ToString()); }); }
虽然变得极其卡,但可以正常显示日期时间了
然而把字符串中的'\0'去掉之后并没有达到我的预期,依旧会很卡,且没有日期时间
查了许多资料,也问了AI很长时间,前前后后花了好几天,最终未能揭开其中的原理不知道是队列的原因还是SerialPort中Read与ReadExisting的区别,或是字符串与字节数组的原因,亦或是多次接收的数据变为合并为一个数据,总之经过了下面变换,可用了,而且打印效率肉眼可见地提升,不再卡顿了。
cs// 原代码: // 在OnDataReceived中 private void OnDataReceived(object? sender, SerialDataReceivedEventArgs e) { if (sender is SerialPort sp) { //ReceiveBytes += sp.BytesToRead; //++ReceiveNum; string data = sp.ReadExisting(); _receiveQueue.Enqueue(data); // 将接收到的数据放入接收队列 //_queueSemaphore.Release(); // 信号量释放,通知有新数据 _resetEvent.Set(); // 通知读取任务开始处理数据 } } // 读取任务 private void ReadTask() { while (_isRunning) { _resetEvent.WaitOne(); // 等待数据接收事件 if (!_isRunning) break; List<string> allData = new List<string>(); while (true) { if (!(_receiveQueue.TryDequeue(out var data))) break; allData.Add(data); if (allData.Count > 10240) break; Thread.Sleep(10); // 适当休眠,避免过度占用CPU } if (allData.Count > 0) { Application.Current.Dispatcher.Invoke(() => { foreach (var item in allData) { TextEditor?.AppendText($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}]>> {item}"); } }); } } }
cs// 改进后: private void OnDataReceived(object? sender, SerialDataReceivedEventArgs e) { if (sender is SerialPort sp) { //ReceiveBytes += sp.BytesToRead; //++ReceiveNum; //string data = sp.ReadExisting(); //_receiveQueue.Enqueue(data); // 将接收到的数据放入接收队列 //_queueSemaphore.Release(); // 信号量释放,通知有新数据 _resetEvent.Set(); // 通知读取任务开始处理数据 } } public async void ReadTask() { const int MaxDataSize = 512; const int BatchSize = 1024; // 批量读取大小 var allData = new List<byte>(); while (true) { _resetEvent.WaitOne(); // 等待数据接收事件 try { while (allData.Count < MaxDataSize && _serialPort.BytesToRead > 0) { var bytesToRead = Math.Min(_serialPort.BytesToRead, BatchSize); var buffer = new byte[bytesToRead]; _serialPort.Read(buffer, 0, bytesToRead); allData.AddRange(buffer); if (allData.Count >= MaxDataSize) break; await Task.Delay(10); // 防止CPU占用过高,使用异步延迟 } } catch { // 处理异常 allData.Clear(); continue; } if (allData.Count > 0) { await LogMessageAsync(_serialPort.Encoding.GetString(allData.ToArray()), DateTime.Now.ToString()); allData.Clear(); // 清空数据列表 } } } private async Task LogMessageAsync(string inputData, string timestamp) { // 使用StringBuilder提高效率 var sb = new StringBuilder(); sb.Append($"[{timestamp}] "); // 使用正则表达式替换换行符,并添加时间戳 var logEntry = Regex.Replace(inputData, @"(\r\n|\r|\n)", $"\r\n{timestamp}>> "); sb.Append(logEntry); // 异步更新UI await Application.Current.Dispatcher.InvokeAsync(() => { TextEditor?.AppendText(sb.ToString()); }); }
出于时间考量不再深究,于此留下痕迹。
2024.11.28:**淦!**用这个可以流畅打印的函数试了之后发现一个不对劲的地方,那就是接收字节数过大,测了一下,发现Python脚本发送数据的真实速度是16359.5次/s,即0.00006s发送一次数据
千算万算没想到是python脚本的问题,不过这倒也解决了"0.015s"与0.016s一次差距悬殊的原因,并且也找到了能高性能显示的方案。真淦
下面是罪魁祸首
pythonimport asyncio import math import random import serial import serial_asyncio class VirtualSerialServer(asyncio.Protocol): def __init__(self, baudrate=115200, interval=0.1, frequency=1.0, amplitude=1.0, offset=0.0): self.transport = None self.baudrate = baudrate self.interval = interval self.frequency = frequency # 正弦波频率(Hz) self.amplitude = amplitude # 正弦波振幅 self.offset = offset # 正弦波偏移量 self.time = 0.0 # 当前时间 def connection_made(self, transport): self.transport = transport print(f'Virtual Serial Port connected with baudrate {self.baudrate}') asyncio.create_task(self.send_data_periodically()) # def data_received(self, data): # print(f'Received: {data.decode()}') # # 回显接收到的数据 # self.transport.write(data) async def send_data_periodically(self): value = 0 while True: # 生成100以内的随机整数 # value = random.randint(0, 100) # 将整数转换为字符串格式 value += 1 data_to_send = f'{value}\n'.encode('utf-8') self.transport.write(data_to_send) # 打印数据 # print(f'Sent: {value}') await asyncio.sleep(self.interval) async def main(): # 固定波特率和时间间隔 baudrate = 115200 interval = 0.015 # 发送间隔(秒) # 使用实际的虚拟串口号,例如 'COM15' port = 'COM15' loop = asyncio.get_running_loop() try: server = await serial_asyncio.create_serial_connection( loop, lambda: VirtualSerialServer(baudrate, interval), url=port, baudrate=baudrate) except serial.serialutil.SerialException as e: print(f"Error opening serial port {port}: {e}") return await asyncio.Event().wait() # 保持程序运行 if __name__ == '__main__': try: asyncio.run(main()) except KeyboardInterrupt: print('程序已终止。')
经过一些测试,发现在异步情况下,Python的定时执行容易出现很奇葩的问题,在我的例子中0.015s就是那个极限,0.016s及其以上却是正常的。
下面是经过改正且可以正常使用的代码
pythonimport time import serial class VirtualSerialServer: def __init__(self, interval=0.015): self.interval = interval self.last_send_time = None # 记录上次发送时间 self.value = 0 # 初始化发送的数据值 def start(self, port, baudrate): try: with serial.Serial(port, baudrate) as ser: print(f'Virtual Serial Port connected with interval {self.interval}') while True: current_time = time.perf_counter() if self.last_send_time is not None: actual_interval = current_time - self.last_send_time print(f'Sent: {self.value}, Actual interval: {actual_interval:.6f} seconds') # 打印实际间隔 # 发送数据 self.value += 1 data_to_send = f'{self.value}\n'.encode('utf-8') ser.write(data_to_send) # 更新最后一次发送时间 self.last_send_time = current_time # 等待直到下一个发送时间点 next_send_time = self.last_send_time + self.interval while time.perf_counter() < next_send_time: pass # 空循环等待直到达到下一个发送时间点 except serial.SerialException as e: print(f"Error opening serial port {port}: {e}") if __name__ == '__main__': interval = 0.012 # 发送间隔(秒) port = 'COM15' # 使用实际的虚拟串口号 baudrate = 115200 # 固定波特率 server = VirtualSerialServer(interval=interval) try: server.start(port, baudrate) except KeyboardInterrupt: print('程序已终止。')