C#串口通讯实战:双线程协作与AutoResetEvent同步机制详解
写C#串口通讯代码,一般是打开串口读数据写数据完成。但实际上如果发了一条命令不等响应就发下一条数据就乱了。所以得发一条等一条。串口等待的时候主线程不能卡死又得保证数据不乱。这就引出了线程同步的问题。本文以 RTU 采集服务器项目中的SerialComm类为例深入分析双线程是怎么协作的AutoResetEvent是怎么用的怎么样处理好串口通信问题。一、代码中的双线程架构1.1 整体设计SerialComm类用了两个BackgroundWorker一个负责发一个负责收线程名称职责轮询间隔发送线程m_AutoExecuteTask从任务队列取命令写入串口200ms接收线程m_AutoReceiveTask轮询串口读取数据500ms为什么要分两个线程因为串口读写是阻塞的。如果用一个线程发完命令等响应响应没来之前就不能干别的或者收数据的时候就不能发命令。分成两个线程收发互不干扰。双线程架构图1.2 启动流程publicvoidStartComm(){this.statustrue;this.OpenSerialPort();this.mSerialPort.DiscardInBuffer();// 清空输入缓冲区if(!this.m_AutoReceiveTask.IsBusy){this.m_AutoReceiveTask.RunWorkerAsync();// 启动接收线程}if(!this.m_AutoExecuteTask.IsBusy){this.m_AutoExecuteTask.RunWorkerAsync();// 启动发送线程}}启动时先打开串口清空缓冲区里的脏数据然后启动两个后台线程。1.3 接收线程主循环privatevoidAutoReceiveTask_DoWork(objectsender,DoWorkEventArgse){BackgroundWorkerbackgroundWorkersenderasBackgroundWorker;while(!backgroundWorker.CancellationPending){if(this.status){try{intnumthis.mSerialPort.Read(this.mReceiveBuffer,0,1024);this.ResetSerialPort();// 定时重置计数if(num0){byte[]arraynewbyte[num];Array.Copy(this.mReceiveBuffer,array,num);ThreadPool.QueueUserWorkItem(newWaitCallback(this.AnalysisData),array);}}catch{// 异常被吞掉了}}Thread.Sleep(500);}e.Canceltrue;}接收线程每 500ms 轮询一次串口读到数据后通过ThreadPool提交给AnalysisData处理。这里用了线程池避免在接收线程里做耗时的解析工作。1.4 发送线程主循环privatevoidAutoExecuteTask(objectsender,DoWorkEventArgse){BackgroundWorkerbackgroundWorkersenderasBackgroundWorker;while(!backgroundWorker.CancellationPending){Thread.Sleep(200);if(this.task.Count!0this.status){this.ExecuteTask();// 发送命令New_newthis.DequeueTask(null,null);// 检查超时任务if(_new!null){if(!_new.WaitRequest_new.ErrorMessage.Length0){ThreadPool.QueueUserWorkItem(newWaitCallback(this.NewSucceedHandler),_new);}else{if(_new.ErrorMessage.Length0){_new.ErrorMessage等待响应超时;}if(_new.AllowRetry_new.RemainTimes0){_new.RemainTimes-1;this.task.Enqueue(_new);// 重新入队}else{ThreadPool.QueueUserWorkItem(newWaitCallback(this.NewErrorHandler),_new);}}}}}e.Canceltrue;}发送线程每 200ms 检查一次任务队列有任务就发送。发送后检查是否有超时未响应的任务超时则重试或报错。二、同步机制分析2.1 核心问题串口通讯的典型流程是发送命令等待设备响应收到响应后处理问题在于发送和接收是两个线程。发送线程发完命令后怎么知道接收线程收到响应了2.2 AutoResetEvent 的用法SerialComm用了AutoResetEvent来解决这个问题privateAutoResetEventmResetEvent;// 构造函数中初始化this.mResetEventnewAutoResetEvent(false);发送线程发完命令后调用WaitOne阻塞等待privatevoidExecuteTask(){New_newthis.task.Obtain();if(_new!null){// ...this.mSerialPort.Write(array,0,array.Length);this.mCurrentTask_new;if(_new.WaitRequest){this.mResetEvent.WaitOne(_new.Timeout*1000,false);// 阻塞等待}}}接收线程解析到响应后调用Set唤醒发送线程privatevoidAnalysisData(objectrecvBytes){// ... 解析数据 ...if(receivedDataEventArgs.Verify){New_newthis.DequeueTask(receivedDataEventArgs.DeviceId,receivedDataEventArgs.MonitorId);if(_new!null){this.mResetEvent.Set();// 唤醒发送线程// ... 处理响应 ...}}}2.3 同步流程图AutoResetEvent 同步时序图AutoResetEvent的特点是Set一次只能唤醒一个WaitOne。唤醒后自动重置为未信号状态。这正好适合发一条等一条的场景。2.4 超时处理如果设备没响应WaitOne会超时返回this.mResetEvent.WaitOne(_new.Timeout*1000,false);超时后发送线程继续执行在AutoExecuteTask中检查到ErrorMessage为空但任务已完成就会标记为等待响应超时。三、防御性编程实践3.1 串口定时重置代码里有一个看起来很奇怪的方法privatevoidResetSerialPort(){this.resetcount;if(this.statusthis.resetcount7200){this.resetcount0;try{this.CloseSerialPort();Thread.Sleep(500);this.OpenSerialPort();}catch{}}}每累计 7200 次读取接收线程每 500ms 读一次7200 次约 1 小时就关闭再重新打开串口。为什么要这么做因为串口硬件长时间运行后可能会出现假死——看起来正常但读写不工作。定时重置是一种防御性措施防止串口卡死导致整个系统瘫痪。3.2 缓冲区溢出保护privatevoidAnalysisData(objectrecvBytes){lock(this.mReceivedData){// 检查缓冲区总长度if(((byte[])recvBytes).Length*2this.mReceivedData.Length65536){this.mReceivedData.Length0;// 清空缓冲区}this.mReceivedData.Append(ConvertEx.ByteArrayToHex((byte[])recvBytes));// ...}}如果缓冲区长度超过 65536直接清空。这是一种粗暴但有效的防内存泄漏手段。正常情况下解析完一帧数据后会从缓冲区移除不会累积。但如果解析出错数据会一直堆积清空可以兜底。3.3 重试机制if(_new.AllowRetry_new.RemainTimes0){_new.RemainTimes-1;this.task.Enqueue(_new);// 重新入队}任务超时或出错时如果允许重试且还有重试次数就重新放回队列。这是一种软失败策略给设备一次重试的机会而不是直接报错。四、存在的问题和解决办法4.1 关于 BackgroundWorkerBackgroundWorker是 .NET 2.0 引入的现在已经不推荐使用了。微软官方建议用Task和async/await替代。但在这个项目里BackgroundWorker用得挺顺手。它自带CancellationPending属性方便控制线程退出RunWorkerAsync一行代码启动比Thread简单。技术选型没有绝对的对错适合场景的就是好的。4.2 关于异常处理代码里有不少空的catch块catch{}在串口通讯场景下串口读写经常会有各种异常设备断开、缓冲区溢出等如果每个异常都处理代码会很复杂所以直接吞掉不处理。但更好的做法是至少记录日志方便排查问题。4.3 关于线程安全AnalysisData方法用了lock (this.mReceivedData)保证缓冲区操作的线程安全。但其他地方比如task队列的操作没有加锁。这是因为task队列的操作都在发送线程里不会有并发问题。而mReceivedData在接收线程和AnalysisData通过线程池调用中都会访问所以需要加锁。线程安全不是到处加锁而是该加的地方加。五、总结串口通讯是半双工的收发要分开。双线程架构是常见做法一个负责发一个负责收。AutoResetEvent适合发一条等一条的同步场景。发送线程WaitOne接收线程Set配合任务队列实现有序通讯。防御性编程很重要。串口定时重置、缓冲区溢出保护、重试机制这些都是应对硬件不确定性的手段。技术选型要看场景。BackgroundWorker虽然老了但在这个项目里用得挺合适。不一定要追新。异常处理不能偷懒。空的catch块虽然省事但出了问题很难排查。至少记个日志。线程安全要精准。不是到处加锁而是分析清楚哪些资源会被并发访问只在那里加锁。关键词C#串口通讯双线程协作、AutoResetEvent、同步机制、生产者-消费者模式、RTU采集本文基于实际项目经验编写代码已脱敏处理。如需完整源码或技术咨询请关注和联系我们。

相关新闻

5分钟快速搭建免费Web邮箱系统:Roundcube Mail完整指南

5分钟快速搭建免费Web邮箱系统:Roundcube Mail完整指南

5分钟快速搭建免费Web邮箱系统:Roundcube Mail完整指南 【免费下载链接】roundcubemail The Roundcube Webmail suite 项目地址: https://gitcode.com/gh_mirrors/ro/roundcubemail 在数字时代,拥有一个专属的Web邮箱系统是个人和企业提升邮件管理…

2026/6/25 20:36:26阅读更多 →
SQL注入深度解析:从原理到实战的攻防指南

SQL注入深度解析:从原理到实战的攻防指南

1. 项目概述:为什么SQL注入依然是头号威胁?干了这么多年网络安全,从甲方到乙方,从渗透测试到应急响应,SQL注入(SQL Injection)这个“老古董”级别的漏洞,我几乎在每一次重大安全事件…

2026/6/25 20:31:24阅读更多 →
Turborepo:用 Rust 加速 JavaScript 项目的构建流程

Turborepo:用 Rust 加速 JavaScript 项目的构建流程

文章目录Turborepo:用 Rust 加速 JavaScript 项目的构建流程monorepo 的构建困境Turborepo 怎么解决的配置简单实际收益适合谁Turborepo:用 Rust 加速 JavaScript 项目的构建流程 一个 JavaScript 项目有几十个子包,每次改一行代码就要重新构…

2026/6/25 20:31:24阅读更多 →
六西格玛黑带培训技术实战:DOE实验设计+多元统计分析+Python代码

六西格玛黑带培训技术实战:DOE实验设计+多元统计分析+Python代码

本文从技术角度,系统讲解六西格玛黑带培训的高级统计工具,适合质量工程师、工艺工程师参考。文末附Python响应曲面设计代码。黑带vs绿带的技术差异绿带只学基础统计工具,黑带要掌握高级统计工具:1. DOE实验设计高级(响…

2026/6/25 23:07:07阅读更多 →
关于代码注释的思考

关于代码注释的思考

书本上的理论以前的笔记里还记着这些理论呢。《重构-改善既有代码的设计》:任何一个傻瓜都能写出计算机可以理解的代码,唯有写出人类容易理解的代码,才是优秀的程序员。《代码整洁之道》上的言论:什么是整洁的代码?1.我…

2026/6/25 23:07:07阅读更多 →
蒙特卡洛离策略强化学习:工业级实操指南

蒙特卡洛离策略强化学习:工业级实操指南

1. 这不是教科书里的“蒙特卡洛离策略”——而是一线强化学习工程师每天真正在调的那套东西“Monte Carlo Off-Policy Explained”这个标题,乍看像一篇理论综述,但如果你真在做机器人控制、广告出价系统、金融交易策略或游戏AI,就会立刻意识到…

2026/6/25 23:07:07阅读更多 →
终极暗黑2存档编辑器:免费网页版角色修改完全指南

终极暗黑2存档编辑器:免费网页版角色修改完全指南

终极暗黑2存档编辑器:免费网页版角色修改完全指南 【免费下载链接】d2s-editor 项目地址: https://gitcode.com/gh_mirrors/d2/d2s-editor 你是否厌倦了重复练级,想要在暗黑破坏神2中快速测试不同的角色build?这款暗黑2存档编辑器正是…

2026/6/25 23:07:07阅读更多 →
3步修复老Mac显卡驱动:OCLP终极优化指南

3步修复老Mac显卡驱动:OCLP终极优化指南

3步修复老Mac显卡驱动:OCLP终极优化指南 【免费下载链接】OpenCore-Legacy-Patcher Experience macOS just like before 项目地址: https://gitcode.com/GitHub_Trending/op/OpenCore-Legacy-Patcher 你是否遇到过这样的场景:将心爱的老Mac升级到…

2026/6/25 23:07:07阅读更多 →
STL到STEP格式转换:工程级3D数据互操作的技术实现

STL到STEP格式转换:工程级3D数据互操作的技术实现

STL到STEP格式转换:工程级3D数据互操作的技术实现 【免费下载链接】stltostp Convert stl files to STEP brep files 项目地址: https://gitcode.com/gh_mirrors/st/stltostp 在现代数字设计与制造工作流中,3D模型格式的兼容性一直是制约设计协作…

2026/6/25 23:02:07阅读更多 →
【人工智能】一文搞定到底什么是智能体

【人工智能】一文搞定到底什么是智能体

【人工智能】一文搞定到底什么是智能体 一文搞定到底什么是智能体【人工智能】一文搞定到底什么是智能体一. LM,WorkFlow,Agent分别有什么么不同二. Agent的思考过程是怎样的三. Agent的五个核心部分1)LLM2)Prompt3)Me…

2026/6/25 9:39:54阅读更多 →
嵌入式GUI控件实战:ROTARY、SCROLLBAR、SLIDER原理与应用

嵌入式GUI控件实战:ROTARY、SCROLLBAR、SLIDER原理与应用

1. 嵌入式GUI控件:从原理到实战的深度解析在嵌入式系统开发中,图形用户界面(GUI)的设计与实现往往是项目从“能用”到“好用”的关键一跃。不同于资源充沛的PC或移动平台,嵌入式设备的GUI需要在有限的CPU性能、内存空间…

2026/6/25 2:52:24阅读更多 →
Google AI Studio 300美元额度的真相与实战指南

Google AI Studio 300美元额度的真相与实战指南

1. 这300美金不是“送钱”,而是Google埋下的第一道技术门槛 你看到标题里那个醒目的“$300美金”时,第一反应可能是:又一个免费额度?领完就完事?我亲手试过——这300美金根本不是红包,而是一张入场券&…

2026/6/25 9:01:34阅读更多 →
面试辅助工具横评:我试了5款AI面试工具,最后留下了OfferGo

面试辅助工具横评:我试了5款AI面试工具,最后留下了OfferGo

上半年跳槽,面了十几家公司。说句实话,不是能力不行,是面试现场太容易崩了。 明明准备了一周,面试官换个问法脑子就一片白。面完之后那个懊悔——其实我会的。 后来开始试市面上的AI面试辅助工具。前前后后装了5款,踩…

2026/6/25 11:52:11阅读更多 →
Claude Code 提示词设计:从塑造“人格”到建立“状态机”

Claude Code 提示词设计:从塑造“人格”到建立“状态机”

当前 AI Agent 设计的核心痛点在于:大模型不缺写代码的能力,缺的是克制力、边界感和验证逻辑。Prompt 不再是用来塑造“人格”的,而是用来建立“状态机(State Machine)”和“行为门禁(Guardrails&#xff0…

2026/6/25 11:52:11阅读更多 →
MC-037 | 自定义 Skill 开发:创建你的AI能力模块

MC-037 | 自定义 Skill 开发:创建你的AI能力模块

MONKEYCODE 教程系列 MonkeyCode教程及推广系列 MC-037 自定义 Skill 开发:创建你的AI能力模块 >官网链接注册更放心哦https://monkeycode-ai.com/?ic019e0aed-c823-783c-b08a-4f030f891e4e 系列: 不爱土豆唯爱马铃薯 MonkeyCode 教程系列 字数: 约 1400 字…

2026/6/25 11:52:11阅读更多 →