RS485 Modbus通信触发Object reference未实例化异常求助
异常根源定位
你遇到的Object reference not set to an instance of an object异常来自.NET底层串口处理逻辑,触发场景是后台串口事件线程尝试访问已被释放的SerialStream对象,结合代码和使用的Modbus库,主要原因有以下几点:
1. 串口断开时的后台事件未被正确终止
调用modbusClient.Disconnect()时,EasyModbus库底层的SerialPort对象被关闭,但SerialStream的后台EventLoopRunner线程可能仍在运行并尝试处理接收事件。此时该线程访问的串口资源已被释放,直接引发空引用异常。
2. 异步代码与同步阻塞的混合滥用
在GetData方法中,通过readTask.ContinueWith(...).Wait()阻塞主线程等待异步操作完成,这种写法会导致线程上下文混乱:
- 后台串口操作线程与主线程的资源调度冲突,加速串口资源的异常释放
ContinueWith的回调执行线程不受控,若涉及资源访问,更容易引发线程安全问题
3. 频繁创建销毁ModbusClient的资源泄漏风险
每次读取都创建新的ModbusClient并执行连接/断开操作,频繁的串口资源申请与释放会导致底层串口驱动的资源管理混乱,增加后台线程资源未清理的概率。
针对性解决方案
1. 安全终止串口后台事件
修改finally块中的断开逻辑,尝试先禁用串口事件再断开连接(若EasyModbus库允许通过反射访问底层SerialPort):
finally { if (modbusClient != null && modbusClient.Connected) { // 反射获取底层SerialPort并取消事件订阅 var serialPort = modbusClient.GetType().GetField("serialPort", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)?.GetValue(modbusClient) as SerialPort; if (serialPort != null) { serialPort.DataReceived -= modbusClient.OnDataReceived; serialPort.ErrorReceived -= modbusClient.OnErrorReceived; } modbusClient.Disconnect(); } _lock.Release(); }
如果库不允许反射访问,可在断开前增加短暂延迟,给后台线程足够时间处理收尾:
modbusClient.Disconnect(); await Task.Delay(100); // 给后台线程留时间终止事件循环
2. 重构异步调用逻辑
将GetData改为异步方法,用await替代ContinueWith + Wait,避免阻塞线程:
public async Task<List<RawInput>> GetDataAsync(DateTime date) { var data = new List<RawInput>(); var items = GetSensors(); var groupeItems = items.GroupBy(i => i.SlaveID).ToList(); foreach (var itemGroup in groupeItems) { var sensors = itemGroup.ToList(); var sensor = sensors.OrderBy(i => i.InputCh).LastOrDefault(); if (sensor == null) continue; try { var result = await modbusManager.ReadInputRegistersAsync( $"COM{sensor.SerPort}", 9600, (byte)itemGroup.Key, 0, sensor.InputCh); data.AddRange(sensors.ConvertAll(s => new RawInput() { ItemId = s.Id, RawValue = result[s.InputCh - 1] })); } catch (Exception ex) { Trace.WriteLine($"读取SlaveID {itemGroup.Key} 失败: {ex.Message}"); data.AddRange(sensors.ConvertAll(s => new RawInput() { ItemId = s.Id, RawValue = 0 })); } } return data; }
3. 复用ModbusClient实例
避免每次读取都创建新客户端,针对每个串口维护一个单例ModbusClient,减少资源频繁创建销毁:
// 维护串口客户端的缓存字典 private readonly Dictionary<string, ModbusClient> _clientCache = new Dictionary<string, ModbusClient>(); private readonly object _cacheLock = new object(); private ModbusClient GetOrCreateClient(string comPort, int baudrate, byte unitIdentifier) { lock (_cacheLock) { if (_clientCache.TryGetValue(comPort, out var client) && client.Connected) { client.UnitIdentifier = unitIdentifier; // 更新从站ID return client; } // 创建新客户端并缓存 var newClient = new ModbusClient(comPort) { UnitIdentifier = unitIdentifier, Baudrate = baudrate, Parity = Parity.None, StopBits = StopBits.One, ConnectionTimeout = 1000 }; newClient.Connect(); _clientCache[comPort] = newClient; return newClient; } }
注意:需要在应用退出时遍历缓存,断开所有客户端连接。
内容的提问来源于stack exchange,提问作者Akbarali Otakhanov

