当前位置: 首页 > news >正文

C#串口通讯实战:双线程协作与AutoResetEvent同步机制详解

写C#串口通讯代码,一般是打开串口,读数据,写数据,完成。

但实际上,如果发了一条命令,不等响应就发下一条,数据就乱了。所以得"发一条等一条"。

串口等待的时候,主线程不能卡死,又得保证数据不乱。这就引出了线程同步的问题。

本文以 RTU 采集服务器项目中的SerialComm类为例,深入分析双线程是怎么协作的,AutoResetEvent是怎么用的,怎么样处理好串口通信问题。


一、代码中的双线程架构

1.1 整体设计

SerialComm类用了两个BackgroundWorker,一个负责发,一个负责收:

线程名称职责轮询间隔
发送线程m_AutoExecuteTask从任务队列取命令,写入串口200ms
接收线程m_AutoReceiveTask轮询串口,读取数据500ms

为什么要分两个线程?因为串口读写是阻塞的。如果用一个线程,发完命令等响应,响应没来之前就不能干别的;或者收数据的时候,就不能发命令。分成两个线程,收发互不干扰。

双线程架构图:

1.2 启动流程

publicvoidStartComm(){this.status=true;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){BackgroundWorkerbackgroundWorker=senderasBackgroundWorker;while(!backgroundWorker.CancellationPending){if(this.status){try{intnum=this.mSerialPort.Read(this.mReceiveBuffer,0,1024);this.ResetSerialPort();// 定时重置计数if(num>0){byte[]array=newbyte[num];Array.Copy(this.mReceiveBuffer,array,num);ThreadPool.QueueUserWorkItem(newWaitCallback(this.AnalysisData),array);}}catch{// 异常被吞掉了}}Thread.Sleep(500);}e.Cancel=true;}

接收线程每 500ms 轮询一次串口,读到数据后通过ThreadPool提交给AnalysisData处理。这里用了线程池,避免在接收线程里做耗时的解析工作。

1.4 发送线程主循环

privatevoidAutoExecuteTask(objectsender,DoWorkEventArgse){BackgroundWorkerbackgroundWorker=senderasBackgroundWorker;while(!backgroundWorker.CancellationPending){Thread.Sleep(200);if(this.task.Count!=0&&this.status){this.ExecuteTask();// 发送命令New_new=this.DequeueTask(null,null);// 检查超时任务if(_new!=null){if(!_new.WaitRequest&&_new.ErrorMessage.Length==0){ThreadPool.QueueUserWorkItem(newWaitCallback(this.NewSucceedHandler),_new);}else{if(_new.ErrorMessage.Length==0){_new.ErrorMessage="等待响应超时!";}if(_new.AllowRetry&&_new.RemainTimes>0){_new.RemainTimes-=1;this.task.Enqueue(_new);// 重新入队}else{ThreadPool.QueueUserWorkItem(newWaitCallback(this.NewErrorHandler),_new);}}}}}e.Cancel=true;}

发送线程每 200ms 检查一次任务队列,有任务就发送。发送后检查是否有超时未响应的任务,超时则重试或报错。


二、同步机制分析

2.1 核心问题

串口通讯的典型流程是:

  1. 发送命令
  2. 等待设备响应
  3. 收到响应后处理

问题在于:发送和接收是两个线程。发送线程发完命令后,怎么知道接收线程收到响应了?

2.2 AutoResetEvent 的用法

SerialComm用了AutoResetEvent来解决这个问题:

privateAutoResetEventmResetEvent;// 构造函数中初始化this.mResetEvent=newAutoResetEvent(false);

发送线程发完命令后,调用WaitOne阻塞等待:

privatevoidExecuteTask(){New_new=this.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_new=this.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.status&&this.resetcount>7200){this.resetcount=0;try{this.CloseSerialPort();Thread.Sleep(500);this.OpenSerialPort();}catch{}}}

每累计 7200 次读取(接收线程每 500ms 读一次,7200 次约 1 小时),就关闭再重新打开串口。

为什么要这么做?因为串口硬件长时间运行后,可能会出现"假死"——看起来正常,但读写不工作。定时重置是一种防御性措施,防止串口卡死导致整个系统瘫痪。

3.2 缓冲区溢出保护

privatevoidAnalysisData(objectrecvBytes){lock(this.mReceivedData){// 检查缓冲区总长度if(((byte[])recvBytes).Length*2+this.mReceivedData.Length>65536){this.mReceivedData.Length=0;// 清空缓冲区}this.mReceivedData.Append(ConvertEx.ByteArrayToHex((byte[])recvBytes));// ...}}

如果缓冲区长度超过 65536,直接清空。这是一种粗暴但有效的防内存泄漏手段。正常情况下,解析完一帧数据后会从缓冲区移除,不会累积。但如果解析出错,数据会一直堆积,清空可以兜底。

3.3 重试机制

if(_new.AllowRetry&&_new.RemainTimes>0){_new.RemainTimes-=1;this.task.Enqueue(_new);// 重新入队}

任务超时或出错时,如果允许重试且还有重试次数,就重新放回队列。这是一种"软失败"策略,给设备一次重试的机会,而不是直接报错。


四、存在的问题和解决办法

4.1 关于 BackgroundWorker

BackgroundWorker是 .NET 2.0 引入的,现在已经不推荐使用了。微软官方建议用Taskasync/await替代。

但在这个项目里,BackgroundWorker用得挺顺手。它自带CancellationPending属性,方便控制线程退出;RunWorkerAsync一行代码启动,比Thread简单。

技术选型没有绝对的对错,适合场景的就是好的。

4.2 关于异常处理

代码里有不少空的catch块:

catch{}

在串口通讯场景下,串口读写经常会有各种异常(设备断开、缓冲区溢出等),如果每个异常都处理,代码会很复杂,所以直接吞掉不处理。

但更好的做法是至少记录日志,方便排查问题。

4.3 关于线程安全

AnalysisData方法用了lock (this.mReceivedData),保证缓冲区操作的线程安全。但其他地方,比如task队列的操作,没有加锁。

这是因为task队列的操作都在发送线程里,不会有并发问题。而mReceivedData在接收线程和AnalysisData(通过线程池调用)中都会访问,所以需要加锁。

线程安全不是"到处加锁",而是"该加的地方加"。


五、总结

  1. 串口通讯是半双工的,收发要分开。双线程架构是常见做法,一个负责发,一个负责收。

  2. AutoResetEvent适合"发一条等一条"的同步场景。发送线程WaitOne,接收线程Set,配合任务队列实现有序通讯。

  3. 防御性编程很重要。串口定时重置、缓冲区溢出保护、重试机制,这些都是应对硬件不确定性的手段。

  4. 技术选型要看场景。BackgroundWorker虽然老了,但在这个项目里用得挺合适。不一定要追新。

  5. 异常处理不能偷懒。空的catch块虽然省事,但出了问题很难排查。至少记个日志。

  6. 线程安全要精准。不是到处加锁,而是分析清楚哪些资源会被并发访问,只在那里加锁。

关键词:C#串口通讯,双线程协作、AutoResetEvent、同步机制、生产者-消费者模式、RTU采集

本文基于实际项目经验编写,代码已脱敏处理。如需完整源码或技术咨询,请关注和联系我们。

http://www.jsqmd.com/news/1077102/

相关文章:

  • Code Llama 70B本地部署与评测实战指南
  • 大模型稀疏激活原理:MoE架构中2%激活率的技术本质
  • 什么是DDS直接数字合成技术?它与传统AWG模式有何区别?
  • 什么是 TaoToken?
  • Shiro反序列化漏洞手工复现:从原理到实战的完整指南
  • VMware替代方案私密评估矩阵首次公开:CPU/内存/存储I/O/热迁移4维打分表,附下载链接
  • 2027最新计算机毕业设计选题推荐
  • Python的__getattr__中的应用AOP
  • 关于图算法中的边松弛与最短路径更新机制的技术7
  • Java毕设项目: 于 SpringBoot 的网上书店管理系统设计与实现 SpringBoot 框架下在线图书销售管理系统设计与实现(源码+文档,讲解、调试运行,定制等)
  • 2026算得准的命理软件推荐怎么看?八字排盘App要看时间规则校验
  • 嵌入向量与向量数据库实战:语义搜索落地核心指南
  • 文件包含漏洞:从代码复用到服务器失控的渗透测试实战解析
  • STM32-S80+RTC时钟+校时+吃药检测+药品分类+药量显示+3次定时+声光提醒+TFT彩屏+(无线方式选择)-3(设计源文件+万字报告+讲解)(支持资料、图片参考_相关定制)_文章底部可以扫码
  • 一文完整拆解 DDoS 攻击全知识点!深度讲解攻击原理、作用方式,附带网站防护方案,全方位搞懂 DDoS 攻防逻辑
  • 【小白向】AI 智能体零基础学习,虾壳云一键部署 OpenClaw v2.7.9 完整拆解教学(最新安装包)
  • 国产老牌羊乳品牌拆解,从产业链看懂产品稳定性
  • Trivy:36k Star 的安全扫描工具,到底好用在哪?
  • 宝可梦存档编辑器终极指南:用PKHeX.Mobile轻松管理你的宝可梦收藏
  • Crossplane:不用写代码就能搭云原生控制平面
  • 利用Burp Collaborator精准检测XXE漏洞的DNS外带攻击
  • 深度学习创新探索
  • Linux系统资源实时监控脚本
  • Vivante图形工具链实战:嵌入式GPU开发从模拟到编译全流程
  • Bugku CTF---简单的RSA
  • 深度剖析:Mos macOS鼠标滚动平滑引擎的源码级架构设计
  • 2026 年自动化测试工具选型指南:8 款主流工具对比
  • 如何用Python实时获取抖音直播间弹幕数据:完整实战指南
  • 验证码自动化测试踩坑实录:轨迹被识破、OCR识别率低?这套优化方案亲测有效
  • FanControl终极调校指南:3步实现电脑风扇从“过山车“到“平稳巡航“的完美转变