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

C# OPC模拟程序随机自动关闭重启问题排查求助

问题描述

我开发了一款对接工业控制系统的C#应用,核心功能是计算数值或读取电子表格数据,再通过EasyOPCClient写入OPC服务器。但运行时出现异常:首次启动后3-5分钟会自动关闭重启,之后每次运行30秒到2分钟就重复关闭重启。关闭前控制台无错误输出,系统事件日志也找不到相关触发记录。这是我第一次做多线程开发,怀疑问题出在线程相关逻辑上,需要排查程序中可能导致该现象的原因。

代码问题排查分析

以下是代码中可能导致程序异常重启的核心问题:

  • 主线程提前退出引发进程不稳定
    Main方法启动线程后没有任何阻塞操作,直接执行完毕。虽然C#前台线程默认会维持进程运行,但主线程退出后进程状态易受系统回收机制影响,导致意外终止。

  • 未捕获异常直接终止进程
    工作线程的异常处理存在大量遗漏:

    • PID方法里的嵌套try块仅捕获部分异常,外层catch覆盖不全;
    • PView方法中的Excel COM操作可能抛出未被捕获的异常(比如文件被占用、版本不兼容);
    • OPC客户端的网络、权限异常可能被静默忽略,一旦出现未处理的线程异常,会直接导致整个进程崩溃。
  • 未托管资源泄漏耗尽系统资源

    • PView方法创建了Excel.Application、Workbook等COM对象,但全程没有释放,长时间运行会导致COM资源泄漏,最终耗尽系统资源引发进程崩溃;
    • 所有工作线程都是无限循环且无退出机制,持续占用CPU、内存等资源,累积到一定程度会被系统强制终止。
  • 线程同步缺失引发写入冲突
    PID和PView线程同时写入同一个OPC标签(比如同一DACA.PV),没有任何同步机制,写入冲突可能引发OPC客户端内部异常,进而导致线程崩溃、进程终止。

  • 异常处理中的不当操作
    PID和PView的catch块里使用Thread.Sleep(300000),会导致线程长时间阻塞,若多个线程同时进入阻塞状态,程序失去响应,可能被系统判定为无响应进程而终止;部分catch块为空,异常被静默忽略,无法排查潜在问题。

修复建议

针对上述问题,可按以下方向修复:

  • 阻塞主线程防止提前退出
    在Main方法最后添加阻塞逻辑,确保工作线程持续运行:

    static void Main(string[] args)
    {
        try
        {
            EasyDAClient client = new EasyDAClient();
            EasyDAClient client2 = new EasyDAClient();
    
            Thread PViewThread = new Thread(() => PView(client));
            Thread PIDThread = new Thread(() => PID(client2));
    
            PViewThread.Start();
            PIDThread.Start();
            
            // 阻塞主线程,避免进程提前终止
            Console.WriteLine("程序运行中,按回车退出...");
            Console.ReadLine();
        }
        catch (Exception e)
        {
            Console.WriteLine("MAIN异常: \n\t" + e);
            Thread.Sleep(5000);
        }
    }
    
  • 完善异常捕获与日志

    • 为每个线程添加顶级try-catch,确保所有异常都被捕获并记录;
    • 把Thread.Sleep(300000)改成短暂休眠(如1000ms)后重试,避免线程长时间阻塞;
    • 移除空catch块,至少记录异常信息,方便排查问题。
  • 释放未托管资源
    在PView方法中添加Excel资源释放逻辑,用finally块确保资源被正确回收:

    public static void PView(EasyDAClient client)
    {
        Excel.Application xLApp = null;
        Excel.Workbook xLWB = null;
        try
        {
            xLApp = new Excel.Application();
            xLWB = xLApp.Workbooks.Open(@"C:\Temp\ParcView_Data_Copy_2.xlsx");
            Excel.Worksheet rawData = xLWB.Sheets[1];
            Excel.Worksheet locData = xLWB.Sheets[2];
            Excel.Range rawDataRange = rawData.UsedRange;
            Excel.Range locDataRange = locData.UsedRange;
    
            int rawDataRow = rawDataRange.Rows.Count;
            int locDataRow = locDataRange.Rows.Count;
    
            string tagName;
            float[] tagValues = new float[locDataRow];
            do
            {
                for (int i = 1; i <= rawDataRow; i++)
                {
                    for (int j = 1; j <= locDataRow; j++)
                    {
                        tagName = locDataRange.Cells[j, 1].Value2.ToString();
                        try
                        {
                            if (rawDataRange.Cells[i, j] != null && rawDataRange.Cells[i, j].Value2 != null)
                                tagValues[j - 1] = float.Parse(rawDataRange.Cells[i, j].Value2.ToString());
                            client.WriteItemValue("", "HWHsc.OPCServer", tagName + ".DACA.PV", tagValues[j - 1]);
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine("PVIEW异常: " + tagName + "\n\t" + e + "\n");
                            Thread.Sleep(1000);
                        }
                    }
                    Thread.Sleep(2000);
                }
            } while (true);
        }
        finally
        {
            // 释放Excel COM资源
            if (xLWB != null)
            {
                xLWB.Close(false);
                System.Runtime.InteropServices.Marshal.ReleaseComObject(xLWB);
            }
            if (xLApp != null)
            {
                xLApp.Quit();
                System.Runtime.InteropServices.Marshal.ReleaseComObject(xLApp);
            }
        }
    }
    
  • 添加线程同步避免写入冲突
    定义全局锁对象,对OPC写入操作加锁,防止多线程同时写入同一标签:

    namespace OPCTest
    {
        public class Program
        {
            // 全局锁对象,用于OPC写入同步
            private static readonly object OpcWriteLock = new object();
            
            // ... 其他方法 ...
            
            // 在写入OPC时使用锁
            lock(OpcWriteLock)
            {
                client.WriteItemValue("", "HWHsc.OPCServer", tagName + ".DACA.PV", tagValues[j - 1]);
            }
        }
    }
    
  • 添加循环终止条件
    使用CancellationToken为无限循环添加优雅退出机制,方便控制线程终止:

    static void Main(string[] args)
    {
        using var cts = new CancellationTokenSource();
        try
        {
            EasyDAClient client = new EasyDAClient();
            EasyDAClient client2 = new EasyDAClient();
    
            Thread PViewThread = new Thread(() => PView(client, cts.Token));
            Thread PIDThread = new Thread(() => PID(client2, cts.Token));
    
            PViewThread.Start();
            PIDThread.Start();
            
            Console.WriteLine("程序运行中,按回车退出...");
            Console.ReadLine();
            // 发送取消信号
            cts.Cancel();
        }
        catch (Exception e)
        {
            Console.WriteLine("MAIN异常: \n\t" + e);
            Thread.Sleep(5000);
        }
    }
    
    // 修改PView方法接收取消令牌
    public static void PView(EasyDAClient client, CancellationToken token)
    {
        // ... 初始化代码 ...
        do
        {
            // 检查是否收到取消信号
            if (token.IsCancellationRequested) break;
            // ... 原有循环逻辑 ...
        } while (true);
    }
    

内容的提问来源于stack exchange,提问作者Hollis Scriber

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.17 13:55:16