_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中的最小部署单元,可以包含一个或…...
SkyWalking 10.2.0 SWCK 配置过程
SkyWalking 10.2.0 & SWCK 配置过程 skywalking oap-server & ui 使用Docker安装在K8S集群以外,K8S集群中的微服务使用initContainer按命名空间将skywalking-java-agent注入到业务容器中。 SWCK有整套的解决方案,全安装在K8S群集中。 具体可参…...

iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...
云计算——弹性云计算器(ECS)
弹性云服务器:ECS 概述 云计算重构了ICT系统,云计算平台厂商推出使得厂家能够主要关注应用管理而非平台管理的云平台,包含如下主要概念。 ECS(Elastic Cloud Server):即弹性云服务器,是云计算…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...
基于服务器使用 apt 安装、配置 Nginx
🧾 一、查看可安装的 Nginx 版本 首先,你可以运行以下命令查看可用版本: apt-cache madison nginx-core输出示例: nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...

关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...
2024年赣州旅游投资集团社会招聘笔试真
2024年赣州旅游投资集团社会招聘笔试真 题 ( 满 分 1 0 0 分 时 间 1 2 0 分 钟 ) 一、单选题(每题只有一个正确答案,答错、不答或多答均不得分) 1.纪要的特点不包括()。 A.概括重点 B.指导传达 C. 客观纪实 D.有言必录 【答案】: D 2.1864年,()预言了电磁波的存在,并指出…...
第25节 Node.js 断言测试
Node.js的assert模块主要用于编写程序的单元测试时使用,通过断言可以提早发现和排查出错误。 稳定性: 5 - 锁定 这个模块可用于应用的单元测试,通过 require(assert) 可以使用这个模块。 assert.fail(actual, expected, message, operator) 使用参数…...

【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...

SpringTask-03.入门案例
一.入门案例 启动类: package com.sky;import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCach…...