_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中的最小部署单元,可以包含一个或…...
小熊猫Dev-C++:5分钟搞定C++开发环境的终极解决方案 [特殊字符]
小熊猫Dev-C:5分钟搞定C开发环境的终极解决方案 🚀 【免费下载链接】Dev-CPP A greatly improved Dev-Cpp 项目地址: https://gitcode.com/gh_mirrors/dev/Dev-CPP 你是否曾为复杂的C开发环境配置而头疼?是否厌倦了臃肿的IDE占用大量系…...
为什么92%的团队用错Gemini做Slides?——基于17家SaaS公司实测数据的生成效率断层分析
更多请点击: https://intelliparadigm.com 第一章:Gemini生成Slides的底层机制与能力边界 Gemini 生成幻灯片(Slides)并非简单地将文本转为 PPT 页面,而是依托多模态大模型对语义结构、视觉层级与演示逻辑的联合建模。…...
秋招编程面试,应届生必备的面试技巧,通过率直接翻倍
文章目录前言一、2026秋招编程面试新趋势:别再用老方法准备,踩坑就出局1.1 八股文不再是核心,底层理解才是硬通货1.2 代码手撕重思路轻结果,工程思维成加分项1.3 项目经历拒绝烂大街,真实落地细节把控是关键二、简历优…...
MCA Selector终极指南:Minecraft世界区块管理的核心技术解析与实战应用
MCA Selector终极指南:Minecraft世界区块管理的核心技术解析与实战应用 【免费下载链接】mcaselector A tool to select chunks from Minecraft worlds for deletion or export. 项目地址: https://gitcode.com/gh_mirrors/mc/mcaselector MCA Selector是一款…...
CSS如何实现一致的圆角半径设计_通过CSS变量存储border-radius
能,但需注意变量作用域、fallback机制及单位完整性;推荐:root定义基础值并用var(--radius-md, 8px),避免嵌套覆盖与无单位变量,旧浏览器需前置静态值。border-radius 用 CSS 变量统一管理,真能省事?能&…...
oh-my-prompt:模块化终端提示符引擎的设计、配置与性能优化
1. 项目概述:一个为现代终端量身定制的提示符引擎如果你和我一样,每天有超过一半的工作时间是在终端(Terminal)里度过的,那么一个高效、美观且信息丰富的命令行提示符(Prompt)绝对能让你事半功倍…...
半导体虚拟计量技术:AI驱动的制造工艺优化
1. 半导体制造中的计量困境与虚拟计量技术崛起 在半导体制造车间里,工程师们每天都要面对一个令人头疼的难题:如何在保证产品质量的同时,又能实时掌握每一片晶圆的工艺状态?传统物理计量方法就像是用显微镜检查大海——虽然精确&a…...
QMCDecode:Mac上最简单的QQ音乐加密音频解密工具
QMCDecode:Mac上最简单的QQ音乐加密音频解密工具 【免费下载链接】QMCDecode QQ音乐QMC格式转换为普通格式(qmcflac转flac,qmc0,qmc3转mp3, mflac,mflac0等转flac),仅支持macOS,可自动识别到QQ音乐下载目录,默认转换结…...
华为OD机试真题 新系统 2026-05-06 JavaGoC语言 实现【匹配命令行前缀关键字】
目录 题目 思路 Code 题目 给定一组命令行字符串和一个命令前缀,需要找出所有以前缀开头的命令行表达式中,前缀之后的第一个关键字,并将这些关键字按字典序排序后返回。 如果找不到匹配前缀则返回空;匹配出多个相同关键字时只返…...
详解 Deepsec:Vercel 开源 AI 代码安全防护工具的技术架构与实现原理
摘要在 AI 大模型深度融入软件开发全链路的今天,代码安全防护正面临 “复杂逻辑漏洞难发现、传统工具误报率高、源码隐私保护难” 三重核心挑战。Vercel 开源的 Deepsec 作为一款Agent 驱动的本地化 AI 安全防护工具,跳出传统 SAST(静态应用安…...




