使用System.IO.Ports的C# WinForms应用运行40分钟后崩溃求助
问题概述
基于C# WinForms开发的应用,使用EasyModbus库通过RS485 RTU协议从多个从站采集数据,运行约40分钟后崩溃,抛出System.NullReferenceException,报错堆栈指向System.IO.Ports的后台事件循环线程。
报错信息
System.NullReferenceException
HResult=0x80004003
Message=Object reference not set to an instance of an object.
Source=System.IO.Ports
StackTrace:
at System.IO.Ports.SerialStream.EventLoopRunner.CallReceiveEvents(Object state)
at System.Threading.QueueUserWorkItemCallbackDefaultContext.Execute()
at System.Threading.ThreadPoolWorkQueue.Dispatch()
at System.Threading.PortableThreadPool.WorkerThread.WorkerThreadStart()
at System.Threading.Thread.StartCallback()
读写核心代码
public int[]? ReadMultipleRegisters(byte nodeID, int startAddress, int quantity) { int[] res=null; int repeat = 0; _mut.WaitOne(); repeat: repeat++; try { _modbusClient.UnitIdentifier = nodeID; res = _modbusClient.ReadHoldingRegisters(startAddress, quantity); } catch (Exception ex) { Debug.Write("Error during Reading multiple registers, repeat:"+repeat+" nodeID:"+nodeID+" stAdr:"+startAddress+" quantity:"+ quantity); Debug.WriteLine(ex.ToString()); if (repeat != _repeats) { goto repeat; } if(repeat==_repeats) { Debug.WriteLine("!#!#!#!#!#READING FAILED!#!#!##!#!#!##!"); } } finally { _mut.ReleaseMutex(); } if(repeat>1) { Debug.WriteLine(repeat+"MORE THAN ONE REPEAT!!!!!!!!!!!"); } return res; } public void WriteSingleRegister(byte id, int address, int value) { try { _mut.WaitOne(); _modbusClient.UnitIdentifier = id; _modbusClient.WriteSingleRegister(address, value); } catch (Exception ex) { //todo ob napaki Debug.Write("Error during Writing to single register, "); Debug.WriteLine(ex.ToString()); } finally { _mut.ReleaseMutex(); } }
可能的原因
- 串口对象生命周期异常:
_modbusClient对应的串口连接被意外释放或置为null,但后台的串口事件循环线程仍在尝试调用相关方法,触发空引用。比如重连时未正确停止原有串口监听就直接替换实例。 - 线程安全漏洞:虽然用
Mutex保护了读写操作,但_modbusClient可能在其他线程(如重连逻辑)被修改,导致读写过程中实例变为null。 - EasyModbus库封装隐患:该库对串口的后台线程处理可能存在线程安全问题,长时间运行后触发空引用。
- 底层串口异常未处理:硬件故障、端口断开等底层异常未被捕获,导致串口内部状态损坏,最终引发空引用。
解决方案
1. 保障_modbusClient生命周期安全
- 禁止在运行中随意替换或销毁
_modbusClient实例。若需重连,先调用_modbusClient.Disconnect(),等待后台线程停止后再创建新实例。 - 在读写方法开头增加实例有效性检查:
if (_modbusClient == null || !_modbusClient.Connected) { Debug.WriteLine("Modbus client is disconnected"); return null; // 写方法直接return }
2. 完善全局线程安全保护
- 对
_modbusClient的创建、销毁、重连操作,也要用同一个Mutex保护,避免读写过程中实例被篡改。 - 确保所有串口操作都在同步上下文内执行,禁止非受控线程直接修改串口对象。
3. 全局捕获未处理异常
由于异常发生在System.IO.Ports的后台线程,普通try-catch无法捕获,需注册全局异常处理事件:
// 应用启动时注册 AppDomain.CurrentDomain.UnhandledException += (sender, e) => { var ex = e.ExceptionObject as Exception; Debug.WriteLine($"Unhandled Exception: {ex?.ToString()}"); // 此处可添加日志记录、自动重连等恢复逻辑 }; Application.ThreadException += (sender, e) => { Debug.WriteLine($"UI Thread Exception: {e.Exception.ToString()}"); // 处理UI线程异常 };
4. 优化读写逻辑
替换goto为循环结构,提升代码可维护性,同时强化异常处理:
public int[]? ReadMultipleRegisters(byte nodeID, int startAddress, int quantity) { int[] res = null; int repeatCount = 0; bool isSuccess = false; while (repeatCount < _repeats && !isSuccess) { _mut.WaitOne(); try { if (_modbusClient == null || !_modbusClient.Connected) { Debug.WriteLine("Modbus client disconnected"); break; } _modbusClient.UnitIdentifier = nodeID; res = _modbusClient.ReadHoldingRegisters(startAddress, quantity); isSuccess = true; } catch (Exception ex) { repeatCount++; Debug.WriteLine($"Read failed (repeat {repeatCount}): NodeID={nodeID}, Start={startAddress}, Quantity={quantity}, Ex={ex}"); } finally { _mut.ReleaseMutex(); } } if (!isSuccess) { Debug.WriteLine("!#!#!#!#!#READING FAILED!#!#!##!#!#!##!"); } else if (repeatCount > 0) { Debug.WriteLine($"{repeatCount + 1} attempts needed for successful read"); } return res; }
5. 检查硬件与串口配置
- 确认RS485转换器稳定性,排查接线松动问题,避免通信过程中硬件断开。
- 调整串口超时时间、波特率等参数,匹配从站设备配置,减少通信异常概率。
内容的提问来源于stack exchange,提问作者Matej Blaško

