Windows服务通过OpcDA写入OPC对象延迟过高问题求助
问题:Windows服务OPC对象更新延迟过长
服务功能
- 调用外部API获取车辆的位置数据与时间戳
- 通过OpcDA协议将时间戳和位置名称写入同一服务器的OPC对象
- 服务以15秒为运行间隔,循环处理约7个对象并执行写入操作
核心问题
服务本身按15秒间隔触发运行,但OPC对象的实际更新间隔长达3-8分钟,更新耗时远超预期。
相关代码
public void OnTimer(object sender, ElapsedEventArgs args) { ConnectToOpc(); Task.Run(RunCoreFunction); Task.Run(ImportData); } private async Task RunCoreFunction() { keepRunning = true; try { await PopulateDevices(); _ = PopulateParalellOpcAsync(); Heartbeat(); } catch (Exception ex) { _logger.Log(ex.Message + ex.StackTrace.ToString(), "RunCoreFunction", LogType.Error); } } // 从外部API获取位置名称和时间戳,存入currCallsResult列表 private async Task PopulateDevices() { try { string _restApiUrl = restApiUrl; currCallsResult = await APIProcessor.LoadDeviceInfoFromPBE(_restApiUrl); foreach (PBEResult obj in currCallsResult) { int result = GetMacInMacArray(obj.SystemId); if (result == -1) { obj.location = null; } else { _logger.Log("Updated entries on " + obj.SystemId + " that is on index: " + result, functionName, LogType.Vehicle); } } } catch (Exception ex) { BreakCoreFunction(ex.Message + " Something wrong with the REST-API"); _logger.Log(ex.Message + " " + ex.InnerException + " " + ex.StackTrace.ToString(), functionName, LogType.Error); } _logger.Log("PopluateDevices Finished, Fetch vehicleData from API", functionName, LogType.Main); } private async Task PopulateParalellOpcAsync() { ConnectToOpc(); // 连接OPC _logger.Log(" PopulateParalellOpcAsync Begins", "PopulateParalellOpcAsync", LogType.Main); if (currCallsResult != null && currCallsResult.Count > 0) { var nrOfItems = macArray.Count(x => x != null && x != "EMPTY"); if (nrOfItems > 0) { List<Task> tasks = new List<Task>(); foreach (PBEResult _obj in currCallsResult) { if (_obj.SystemId != null) { // 写入OPC对象 tasks.Add(Task.Run(() => PopulateOpc(_obj))); } } await Task.WhenAll(tasks); } } else { _logger.Log(" Can't populate OPC, currCallsResult from the API is empty", "PopulateParalellOpcAsync", LogType.Main); } } private async Task<string> PopulateOpc(PBEResult obj) // 写入OPC对象 { int assetID = 0; string _checkTag2 = ""; if (obj.SystemId != null) { assetID = GetMacInMacArray(obj.SystemId); if (assetID > 0) { try { ConnectToOpc(); _checkTag2 = await opcMngr.ReadTagAsync(opcTag + assetID + ".LocationData" + ".MacAddress"); if (!(String.IsNullOrEmpty(_checkTag2))) { opcMngr.WriteStringTagAsync(opcTag + assetID + ".LocationData" + ".Timestamp", obj.TimeStamp.ToString()); opcMngr.WriteStringTagAsync(opcTag + assetID + ".LocationData" + ".LocationName", obj.location); _logger.Log(" OPC Tags Updated AssetName: " + obj.AssetName, functionName, LogType.Main); } else { _logger.Log("Error, Look at Error log file ", functionName, LogType.Main); } } catch (Exception e) { _logger.Log("Error, Look at Error log file ", functionName, LogType.Main); } } } else { _logger.Log(", SystemId is null " + obj.AssetName, functionName, LogType.Main); } return null; } private void ConnectToOpc() { if ((opcMngr == null) || (opcMngr.Status != "Connected")) // 检查opcMngr是否存在或已连接 { _logger.Log("Connecting to OPC", "ConnectToOPC", LogType.Main); opcMngr = new OpcManager(opcUri); _logger.Log("Connected!", "ConnectToOPC", LogType.Main); } }
问题分析
异步操作未等待完成:
RunCoreFunction中调用_ = PopulateParalellOpcAsync();未使用await,导致OPC写入任务在后台异步执行,下一次定时器触发时,上一次的写入操作可能还未完成,造成任务堆积。PopulateOpc中调用WriteStringTagAsync未使用await,写入操作可能还未完成就结束方法,导致写入被后台挂起,延迟生效。
OPC连接管理混乱:
ConnectToOpc在定时器回调、PopulateParalellOpcAsync、PopulateOpc中被多次调用,多线程环境下可能重复创建连接,增加连接开销;且未加锁,连接状态判断可能不准确。
定时器任务并发失控:
- 定时器每15秒触发一次新任务,若前一次任务未执行完毕,会导致大量任务堆积,OPC服务器无法及时处理所有请求,进而引发更新延迟。
不必要的OPC读取操作:
PopulateOpc中每次写入前都读取MacAddress标签,若该操作并非必要,会额外增加OPC服务器的请求压力。
解决方案
1. 确保所有异步操作等待完成
- 修改
RunCoreFunction中的调用,等待OPC写入完成:await PopulateParalellOpcAsync(); - 在
PopulateOpc中等待写入操作完成:await opcMngr.WriteStringTagAsync(opcTag + assetID + ".LocationData" + ".Timestamp", obj.TimeStamp.ToString()); await opcMngr.WriteStringTagAsync(opcTag + assetID + ".LocationData" + ".LocationName", obj.location);
2. 优化OPC连接管理
- 加锁确保多线程下不会重复创建连接,并释放旧连接:
private readonly object _opcLock = new object(); private void ConnectToOpc() { lock (_opcLock) { if ((opcMngr == null) || (opcMngr.Status != "Connected")) { _logger.Log("Connecting to OPC", "ConnectToOPC", LogType.Main); opcMngr?.Dispose(); // 释放旧连接资源 opcMngr = new OpcManager(opcUri); _logger.Log("Connected!", "ConnectToOPC", LogType.Main); } } } - 建议在服务启动时初始化一次连接,后续仅在连接断开时重新连接,减少重复连接的开销。
3. 控制定时器任务并发
- 添加标志位,避免前一次任务未完成时触发新任务:
private bool _isProcessing = false; private readonly object _processingLock = new object(); public void OnTimer(object sender, ElapsedEventArgs args) { lock (_processingLock) { if (_isProcessing) return; _isProcessing = true; } Task.Run(async () => { try { ConnectToOpc(); await RunCoreFunction(); await ImportData(); } finally { lock (_processingLock) { _isProcessing = false; } } }); }
4. 移除不必要的OPC读取
- 如果
MacAddress标签的读取并非写入的必要前置条件,直接移除该读取操作,减少OPC服务器压力:// 移除以下代码 // _checkTag2 = await opcMngr.ReadTagAsync(opcTag + assetID + ".LocationData" + ".MacAddress"); // if (!(String.IsNullOrEmpty(_checkTag2))) // { // ... // }
内容的提问来源于stack exchange,提问作者B.j
相关产品推荐
相关产品推荐

