You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
    }                
}

问题分析

  1. 异步操作未等待完成:

    • RunCoreFunction中调用_ = PopulateParalellOpcAsync();未使用await,导致OPC写入任务在后台异步执行,下一次定时器触发时,上一次的写入操作可能还未完成,造成任务堆积。
    • PopulateOpc中调用WriteStringTagAsync未使用await,写入操作可能还未完成就结束方法,导致写入被后台挂起,延迟生效。
  2. OPC连接管理混乱:

    • ConnectToOpc在定时器回调、PopulateParalellOpcAsync、PopulateOpc中被多次调用,多线程环境下可能重复创建连接,增加连接开销;且未加锁,连接状态判断可能不准确。
  3. 定时器任务并发失控:

    • 定时器每15秒触发一次新任务,若前一次任务未执行完毕,会导致大量任务堆积,OPC服务器无法及时处理所有请求,进而引发更新延迟。
  4. 不必要的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 21:47:31