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

