_C#_串口助手_字符串拼接缺失问题(未知原理)
最近使用WPF开发串口助手时,遇到一个很奇怪的问题,无论是主线程、异步还是多线程,当串口接收速度达到0.016s一次以上,就会发生字符串缺失问题并且很卡。而0.016s就一切如常,仿佛0.015s与0.016s是天堑之隔。
同一份代码放在多线程(主线程或异步)环境中:
由于是在测试,尝试过用索引下标的方式遍历数组
其中allData的定义为
List<string> allData = new List<string>();
if (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里的数据先转为数字,再直接拼接字符串:
if (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的区别,或是字符串与字节数组的原因,亦或是多次接收的数据变为合并为一个数据,总之经过了下面变换,可用了,而且打印效率肉眼可见地提升,不再卡顿了。
// 原代码:// 在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}");}});}}}
// 改进后: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);// 异步更新UIawait Application.Current.Dispatcher.InvokeAsync(() =>{TextEditor?.AppendText(sb.ToString());});}
出于时间考量不再深究,于此留下痕迹。
2024.11.28:
淦!用这个可以流畅打印的函数试了之后发现一个不对劲的地方,那就是接收字节数过大,测了一下,发现Python脚本发送数据的真实速度是16359.5次/s,即0.00006s发送一次数据
千算万算没想到是python脚本的问题,不过这倒也解决了“0.015s”与0.016s一次差距悬殊的原因,并且也找到了能高性能显示的方案。真淦
下面是罪魁祸首
import asyncio import math import randomimport serial import serial_asyncioclass VirtualSerialServer(asyncio.Protocol):def __init__(self, baudrate=115200, interval=0.1, frequency=1.0, amplitude=1.0, offset=0.0):self.transport = Noneself.baudrate = baudrateself.interval = intervalself.frequency = frequency # 正弦波频率(Hz)self.amplitude = amplitude # 正弦波振幅self.offset = offset # 正弦波偏移量self.time = 0.0 # 当前时间def connection_made(self, transport):self.transport = transportprint(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 = 0while True:# 生成100以内的随机整数# value = random.randint(0, 100)# 将整数转换为字符串格式value += 1data_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 = 115200interval = 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}")returnawait asyncio.Event().wait() # 保持程序运行if __name__ == '__main__':try:asyncio.run(main())except KeyboardInterrupt:print('程序已终止。')
经过一些测试,发现在异步情况下,Python的定时执行容易出现很奇葩的问题,在我的例子中0.015s就是那个极限,0.016s及其以上却是正常的。
下面是经过改正且可以正常使用的代码
import time import serialclass VirtualSerialServer:def __init__(self, interval=0.015):self.interval = intervalself.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_timeprint(f'Sent: {self.value}, Actual interval: {actual_interval:.6f} seconds') # 打印实际间隔# 发送数据self.value += 1data_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.intervalwhile 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('程序已终止。')
相关文章:

_C#_串口助手_字符串拼接缺失问题(未知原理)
最近使用WPF开发串口助手时,遇到一个很奇怪的问题,无论是主线程、异步还是多线程,当串口接收速度达到0.016s一次以上,就会发生字符串缺失问题并且很卡。而0.016s就一切如常,仿佛0.015s与0.016s是天堑之隔。 同一份代码…...
浅析大数据时代下的网络安全
一、大数据时代下网络安全的现状 在全球化进程不断深入发展的情况下,互联网行业发展速度也更加迅猛,人们对网络信息的需求量不断增加,所以目前已经进入了大数据时代。 随着计算机技术的不断发展,我国互联网网络规模、网民数量、…...

Mysql数据库基础篇笔记
目录 sql语句 DDL——数据库定义语言(定义库,表,字段) 数据库操作: 表操作: DML 增删改语句 DQL 语法编写顺序: 条件查询 DCL 用户管理: 权限管理: 函数 常见字符串内置函…...

rabbitmq原理及命令
目录 一、RabbitMQ原理1、交换机(Exchange)fanoutdirecttopicheaders(很少用到) 2、队列Queue3、Virtual Hosts4、基础对象 二、RabbitMQ的一些基本操作:1、用户管理2、用户角色3、vhost4、开启web管理接口5、批量删除队列 一、Ra…...
React进阶面试题(四)
React 的 reconciliation(协调)算法 Reconciliation是React的diff算法,用于比较更新前后的虚拟DOM树差异,从而使用最小的代价将原始DOM按照新的状态、属性进行更新。其目的是找出两棵树的差异,原生方式直接比较复杂度…...

24/12/1 算法笔记<强化学习> 创建Maze交互
我们今天制作一个栅格的游戏。 我们直接上代码教学。 1.载入库和查找相应的函数版本 import numpy as np import time import sysif sys.version_info.major 2:import Tkinter as tk else:import tkinter as tk 2.设置长宽和单元格大小 UNIT 40 MAZE_H 4 MAZE_W 4 3.初始…...

Linux驱动开发(10):I2C子系统–mpu6050驱动实验
本章我们以板载MPU6050为例讲解i2c驱动程序的编写,本章主要分为五部分内容。 第一部分,i2c基本知识,回忆i2c物理总线和基本通信协议。 第二部分,linux下的i2c驱动框架。 第三部分,i2c总线驱动代码拆解。 第四部分&a…...

《装甲车内气体检测“神器”:上海松柏 K-5S 电化学传感器模组详解》
《装甲车内气体检测“神器”:上海松柏 K-5S 电化学传感器模组详解》 一、引言二、K-5S 电化学传感器模组概述(一)产品简介(二)产品特点(三)产品适用场景 三、电化学传感器原理及优点(一…...
如何将多个JS文件打包成一个JS文件?
文章目录 前言SDK 打包安装 webpack创建 webpack.config.js编译命令行遇到的坑点前言 上一篇已经记录了如何开发一个小游戏聚合SDK,既然是SDK,最终都是给外部人员使用的。调研了一下市面上的前端SDK,最终都是编译成一个 js 文件。我猜理由大概是 js 文件之间的调用都是需要…...
100个python经典面试题详解(新版)
应老粉要求,每晚加餐一个最新面试题 包括Python面试中常见的问题,涵盖列表、元组、字符串插值、比较操作符、装饰器、类与对象、函数调用方式、数据结构操作、序列化、数据处理函数等多个方面。 旨在帮助数据科学家和软件工程师准备面试或提升Python技能。 7、Python面试题…...

C#初阶概念理解
梳理了一些本人在学习C#时的一些生疏点,同时也加深自己的印象。 堆&栈 堆用来存储程序运行时产生的变量,当程序结束时释放; 栈用来存储程序运行时,调用方法产生的临时变量,方法运行完成后就会释放…...

node.js基础学习-url模块-url地址处理(二)
前言 前面我们创建了一个HTTP服务器,如果只是简单的http://localhost:3000/about这种链接我们是可以处理的,但是实际运用中一般链接都会带参数,这样的话如果我们只是简单的判断链接来分配数据,就会报404找不到链接。为了解决这个问…...

算法与数据结构(1)
一:数据结构概论 数据结构分为初阶数据结构(主要由C语言实现)和高阶数据结构(由C实现) 初阶数据结构当中,我们会学到顺序表、链表、栈和队列、二叉树、常见排序算法等内容。 高阶数据结构当中࿰…...

FTP介绍与配置
前言: FTP是用来传送文件的协议。使用FTP实现远程文件传输的同时,还可以保证数据传输的可靠性和高效性。 介绍 FTP的应用 在企业网络中部署一台FTP服务器,将网络设备配置为FTP客户端,则可以使用FTP来备份或更新VRP文件和配置文件…...
SQL面试题——抖音SQL面试题 最近一笔有效订单
最近一笔有效订单 题目背景如下,现有订单表order,包含订单ID,订单时间,下单用户,当前订单是否有效 +---------+----------------------+----------+-----------+ | ord_id | ord_time | user_id | is_valid | +---------+----------------------+--------…...

【线程】Java多线程代码案例(1)
【线程】Java多线程代码案例(1) 一、“单例模式” 的实现1.1“饿汉模式”1.2 “懒汉模式”1.3 线程安全问题 二、“阻塞队列”的实现2.1阻塞队列2.2生产者消费者模型2.3 阻塞队列的实现2.4 再谈生产者消费者模型 一、“单例模式” 的实现 “单例模式”即…...

go使用mysql实现增删改查操作
1、安装MySQL驱动 go get -u github.com/go-sql-driver/mysql2、go连接MySQL import ("database/sql""log"_ "github.com/go-sql-driver/mysql" // 导入 mysql 驱动 )type Users struct {ID intName stringEmail string }var db *sql.DBfu…...

【Rust】unsafe rust入门
这篇文章简单介绍下unsafe rust的几个要点 1. 解引用裸指针 裸指针其实就是C或者说C的指针,与C的指针不同的是,Rust的裸指针还是要分为可变和不可变,*const T 和 *mut T: 基于引用创建裸指针 let mut num 5;let r1 &num …...
dpwwn02靶场
靶机下载地址:https://download.vulnhub.com/dpwwn/dpwwn-02.zip 信息收集 ip add 查看kali Linux虚拟机的IP为:10.10.10.128 https://vulnhub.com/entry/dpwwn-2,343/中查看靶机的信息,IP固定为10.10.10.10 所以kali Linux添加仅主机网卡…...
K8S疑难概念理解——Pod,应该以哪种Kind来部署应用,为什么不直接Pod这种kind?
文章目录 一、Pod概念深度理解,为什么一般不直接以kindPod资源类型来部署应用?二、究竟应该以哪种资源类型来部署应用 一、Pod概念深度理解,为什么一般不直接以kindPod资源类型来部署应用? Pod是Kubernetes中的最小部署单元,可以包含一个或…...

JavaSec-RCE
简介 RCE(Remote Code Execution),可以分为:命令注入(Command Injection)、代码注入(Code Injection) 代码注入 1.漏洞场景:Groovy代码注入 Groovy是一种基于JVM的动态语言,语法简洁,支持闭包、动态类型和Java互操作性,…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院挂号小程序
一、开发准备 环境搭建: 安装DevEco Studio 3.0或更高版本配置HarmonyOS SDK申请开发者账号 项目创建: File > New > Create Project > Application (选择"Empty Ability") 二、核心功能实现 1. 医院科室展示 /…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
基于数字孪生的水厂可视化平台建设:架构与实践
分享大纲: 1、数字孪生水厂可视化平台建设背景 2、数字孪生水厂可视化平台建设架构 3、数字孪生水厂可视化平台建设成效 近几年,数字孪生水厂的建设开展的如火如荼。作为提升水厂管理效率、优化资源的调度手段,基于数字孪生的水厂可视化平台的…...
爬虫基础学习day2
# 爬虫设计领域 工商:企查查、天眼查短视频:抖音、快手、西瓜 ---> 飞瓜电商:京东、淘宝、聚美优品、亚马逊 ---> 分析店铺经营决策标题、排名航空:抓取所有航空公司价格 ---> 去哪儿自媒体:采集自媒体数据进…...
rnn判断string中第一次出现a的下标
# coding:utf8 import torch import torch.nn as nn import numpy as np import random import json""" 基于pytorch的网络编写 实现一个RNN网络完成多分类任务 判断字符 a 第一次出现在字符串中的位置 """class TorchModel(nn.Module):def __in…...
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
以下是一个完整的 Angular 微前端示例,其中使用的是 Module Federation 和 npx-build-plus 实现了主应用(Shell)与子应用(Remote)的集成。 🛠️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...

安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...

C# 表达式和运算符(求值顺序)
求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如,已知表达式3*52,依照子表达式的求值顺序,有两种可能的结果,如图9-3所示。 如果乘法先执行,结果是17。如果5…...

PHP 8.5 即将发布:管道操作符、强力调试
前不久,PHP宣布了即将在 2025 年 11 月 20 日 正式发布的 PHP 8.5!作为 PHP 语言的又一次重要迭代,PHP 8.5 承诺带来一系列旨在提升代码可读性、健壮性以及开发者效率的改进。而更令人兴奋的是,借助强大的本地开发环境 ServBay&am…...