(深入探讨句柄泄漏、内存泄漏检测工具)以及 TestSection 和 PowerSupply 类的代码,我将分析 PowerSupply 类中可能导致内存泄漏(包括托管内存和非托管内存,如句柄泄漏)的潜在问题,重点关注线程管理、事件订阅、集合分配和日志写入等方面。接着,我将提供优化建议和具体代码示例,解决这些问题,并确保与 TestSection 的协作逻辑一致。以下内容全部用中文解释,结合 .NET Framework 4.0 的 Windows Forms 应用程序(ROBOT.exe)场景,清晰易懂。
一、PowerSupply 类中的内存泄漏问题分析
PowerSupply 类是一个硬件控制类,负责电源设备的初始化、电压设置、状态查询和错误处理。结合代码和上下文,以下是可能导致内存泄漏(托管和非托管)的风险点:
- 线程句柄泄漏
-
相关代码:
csharp
public void StopPower() { Thread thr = new Thread(new ThreadStart(_StopPower)) { IsBackground = true }; thr.Start(); } public void Reset() { Thread thr = new Thread(new ThreadStart(_Reset)) { IsBackground = true }; thr.Start(); } public void StartQuery() { Thread thr = new Thread(new ThreadStart(_StartQuery)) { IsBackground = true }; thr.Start(); } public void StopQuery() { Thread thr = new Thread(new ThreadStart(_StopQuery)) { IsBackground = true }; thr.Start(); } -
问题:
-
频繁创建线程:StopPower、Reset、StartQuery 和 StopQuery 每次调用都创建新线程(new Thread),分配线程句柄(约 100-200 字节非托管内存)。
-
未显式终止:线程依赖 StoppedQuery 或 SinySystem.InterLock 退出循环,但未保证立即终止,可能因 Thread.Sleep(1000) 阻塞而延迟释放句柄。
-
多线程并发:多个 PowerSupply 实例(在 TestSection.PowerSupplyMap 中)可能同时调用这些方法,导致线程句柄累积。
-
与 TestSection 的交互:TestSection.QuicklyStopTest 调用 PowerSupply.ProtectPower,可能触发 StartQuery 或 StopPower,增加线程创建。
-
-
影响:
-
线程句柄泄漏增加非托管内存占用,耗尽进程句柄限制(默认 10,000)。
-
可能导致 System.OutOfMemoryException,尤其在高频操作(如频繁启动/停止测试)时。
-
- 事件订阅未清理
-
相关代码:
csharp
public event PowerError PowerError;-
在 TestSection.Initialize 中订阅:
csharp
foreach (PowerSupply powerSupply in PowerSupplyMap.Values) { powerSupply.PowerError += PowerSupply_PowerError; }
-
-
问题:
-
PowerSupply.PowerError 事件未在 TestSection.Uninitialize 或 PowerSupply.Uninitialize 中移除,可能导致 PowerSupply 实例被 TestSection 持有,阻止 GC 回收。
-
如果 PowerSupplyMap 中的实例被替换或重用,但旧实例未清理,事件订阅可能累积。
-
-
影响:
-
托管内存泄漏:PowerSupply 实例无法回收,增加托管堆大小。
-
非托管内存泄漏:PowerSupply 的 m_Hardware(IHardwareAdaptor)可能持有句柄(如串口或设备句柄),未释放。
-
- 集合分配与内存管理
-
相关代码:
csharp
private Dictionary<string, object> m_DisplayMap = new Dictionary<string, object>(); private void InitDataMap() { if (Name.Contains("GBPS")) { m_DisplayMap = new Dictionary<string, object> { { "VGETIME", new List<double>() }, { "VGEPSRealV", new List<double>() }, }; } // ... }-
CheckPowerIG 中的临时集合:
csharp
Dictionary<string, double> ChannelMAXICMap = new Dictionary<string, double>();
-
-
问题:
-
m_DisplayMap:每次调用 InitDataMap 都创建新 Dictionary 和 List<double>,未清理旧实例,可能导致内存累积。
-
ChannelMAXICMap:在 CheckPowerIG 中频繁创建临时 Dictionary,未显式释放(虽由 GC 管理,但高频分配增加压力)。
-
DataTable:CreateStopDatatable、ProtectPower 和 ReplyPower 每次创建新的 DataTable,未重用,可能增加托管内存分配。
-
-
影响:
-
频繁分配 Dictionary、List 和 DataTable 增加 GC 压力(Gen0 回收频繁)。
-
如果 m_DisplayMap 的 List<double> 未清空,可能累积大量数据。
-
- GDI 句柄潜在风险
-
相关代码:未直接涉及 UI,但 PowerSupply 的状态(如 PSRealTimeState)可能通过 TestSection.UpdataMonitorData 更新 UI(如 labelPercent.Text)。
-
问题:
-
如果 SystemNotifEvent.HardInfoNotifEvent 或 HardErrorNotifEvent 触发 UI 更新(如控件 Visible 或 Text),可能增加 GDI 句柄。
-
频繁调用 ReadV(每秒一次,Thread.Sleep(1000))可能导致 UI 消息队列积压(MethodInvoker 对象)。
-
-
影响:
- GDI 句柄泄漏可能导致 OutOfMemoryException,尤其在高频 UI 更新场景。
- 文件句柄与日志写入
-
相关代码:
csharp
private static readonly ILog m_Log = LogManager.GetLogger("ROBOT"); m_Log.Info(Name + " : " + instrction); -
问题:
-
log4net 的 FileAppender 在高频写入(如 ReadV 每秒调用)时,可能未及时释放文件句柄。
-
如果日志文件配置不当(例如,缺少 maxSizeRollBackups),可能导致文件句柄泄漏。
-
-
影响:
- 文件句柄泄漏累积,增加非托管内存占用。
- 非托管资源管理
-
相关代码:
csharp
private IHardwareAdaptor m_Hardware = null; m_Hardware = IoManager.Hardware(Com); m_Hardware.Initialize(); m_Hardware.OpenSession(); -
问题:
-
m_Hardware(IHardwareAdaptor)可能管理非托管资源(如串口、设备句柄),但 Uninitialize 未始终正确关闭(例如,异常路径可能跳过 CloseSession)。
-
如果 m_Hardware 未实现 IDisposable,可能导致句柄泄漏。
-
-
影响:
- 设备句柄泄漏增加非托管内存,影响进程稳定性。
二、优化 PowerSupply 防止内存泄漏
以下是针对 PowerSupply 类的优化方案,解决线程句柄、事件订阅、集合管理、GDI 句柄、文件句柄和非托管资源问题,与 TestSection 协作优化。
- 使用 CancellationToken 管理线程
-
问题:StopPower、Reset、StartQuery 和 StopQuery 创建线程,未显式终止。
-
优化:
-
使用 CancellationTokenSource 控制线程生命周期。
-
合并线程逻辑,减少线程创建。
-
-
代码:
csharp
private CancellationTokenSource cts = new CancellationTokenSource(); public void StartQuery() { cts?.Dispose(); cts = new CancellationTokenSource(); Thread thr = new Thread(() => _StartQuery(cts.Token)) { IsBackground = true }; thr.Start(); } private void _StartQuery(CancellationToken token) { try { if (m_Hardware == null) { Initialize(); if (m_Hardware == null) { ErrExceptionGrad(UNFINDHARDWARE); return; } } if (!m_Hardware.Connected) { ErrExceptionGrad(UNCONNECTIONHARDWARE); return; } FirstRate = true; while (!token.IsCancellationRequested && SinySystem.InterLock == 0) { RateQurey = true; ReadV(); Thread.Sleep(1000); } } catch (Exception ex) { ExceptionDoEvnets(ex); SystemNotifEvent.HardErrorNotifEvent(this, Name + ": 巡检状态异常。", ""); } finally { SystemNotifEvent.HardInfoNotifEvent(this, Name + ": 已退出状态巡检", false); m_Dis = true; StoppedQuery = true; } } public void StopQuery() { cts?.Cancel(); Thread thr = new Thread(() => _StopQuery(cts.Token)) { IsBackground = true }; thr.Start(); } private void _StopQuery(CancellationToken token) { try { if (m_Hardware == null || !m_Hardware.Connected) { m_Dis = true; return; } if (HardwareConnectStatus == HardwareConnectStatus.InUsing && PSRealTimeState.PSRealVG > 2) { HardwareResult result = m_Hardware.QueryData("PSSET " + CheckGBPS(CreateStopDatatable())) as HardwareResult; m_Log.Info(Name + " PSSET Stop"); ProcessError(result, HardwareConnectStatus.InUsing); } Uninitialize(); } catch (Exception ex) { ExceptionDoEvnets(ex); SystemNotifEvent.HardErrorNotifEvent(this, Name + ": 停止状态巡检失败。", ""); } finally { m_Dis = true; StoppedQuery = true; } } public void StopPower() { cts?.Cancel(); Thread thr = new Thread(() => _StopPower(cts.Token)) { IsBackground = true }; thr.Start(); } private void _StopPower(CancellationToken token) { try { if (m_Hardware == null || !m_Hardware.Connected) return; if (HardwareConnectStatus == HardwareConnectStatus.InUsing) { StopExcute = true; HardwareResult result = m_Hardware.QueryData("PSSET " + CheckGBPS(CreateStopDatatable())) as HardwareResult; m_Log.Info(Name + " PSSET Stop"); ProcessError(result, HardwareConnectStatus.InUsing); } } catch (Exception ex) { ExceptionDoEvnets(ex); SystemNotifEvent.HardErrorNotifEvent(this, Name + ": 停止状态巡检失败。", ""); } finally { Uninitialize(); StopExcute = true; } } public void Reset() { cts?.Cancel(); Thread thr = new Thread(() => _Reset(cts.Token)) { IsBackground = true }; thr.Start(); } private void _Reset(CancellationToken token) { ActionComplete = true; ProtectPower(); } -
解释:
-
使用单一 CancellationTokenSource 控制所有线程,cts.Cancel() 确保线程可控终止。
-
减少线程创建,合并逻辑(如 StopPower 和 StopQuery 共享 Uninitialize)。
-
在 TestSection.QuicklyStopTest 中调用 PowerSupply.StopQuery 和 ProtectPower,确保线程统一取消。
-
- 实现 IDisposable 清理事件和资源
-
问题:PowerError 事件未移除,m_Hardware 和 m_DisplayMap 未清理。
-
优化:
- 实现 IDisposable,清理事件订阅、集合和硬件资源。
-
代码:
csharp
public class PowerSupply : Hardware, IHardwareFlow, IDisposable { private bool disposed = false; public void Dispose() { if (!disposed) { PowerError = null; // 移除所有事件订阅 m_DisplayMap?.Clear(); errorMap?.Clear(); cts?.Dispose(); if (m_Hardware != null) { try { m_Hardware.CloseSession(); m_Hardware.UnInitialize(); } catch (Exception ex) { m_Log.Error($"{Name}: Dispose 失败 - {ex.Message}"); } m_Hardware = null; } m_sp?.Stop(); m_sp2?.Stop(); m_sp3?.Stop(); disposed = true; m_Log.Info($"{Name}: Disposed"); GC.SuppressFinalize(this); } } protected override void Finalize() { Dispose(); } } -
TestSection 协作:
-
在 TestSection.Dispose 中调用 PowerSupply.Dispose:
csharp
public class TestSection : Hardware, IHardwareFlow, IDisposable { public void Dispose() { if (!disposed) { foreach (TestChannel testChannel in TestChannelMap.Values) { testChannel.ChannelProtect -= TestChannel_ChannelProtect; testChannel.ChannelError -= TestChannel_ChannelError; testChannel.Dispose(); // 假设 TestChannel 也实现 IDisposable } foreach (PowerSupply powerSupply in PowerSupplyMap.Values) { powerSupply.PowerError -= PowerSupply_PowerError; powerSupply.Dispose(); } foreach (Press press in PressMap.Values) { press.PressError -= Press_PressError; press.Dispose(); // 假设 Press 实现 IDisposable } TestChannelMap.Clear(); PowerSupplyMap.Clear(); PressMap.Clear(); while (Data.TryDequeue(out _)) { } ResultData.Clear(); cts?.Dispose(); disposed = true; m_Log.Info($"TestSection {Id} Disposed"); GC.SuppressFinalize(this); } } }
-
-
解释:
-
移除 PowerError 事件订阅,防止 PowerSupply 被 TestSection 持有。
-
清空 m_DisplayMap 和 errorMap,释放托管内存。
-
确保 m_Hardware 关闭会话,释放设备句柄。
-
在窗体关闭时调用:
csharp
protected override void OnFormClosing(FormClosingEventArgs e) { base.OnFormClosing(e); LogManager.Shutdown(); timerManager?.Dispose(); SelectControlTestUnitSelecter.TestSection?.Dispose(); ChannelChartsMap?.Clear(); ControlTestUnitSelecterMap?.Clear(); }
-
- 优化集合管理
-
问题:m_DisplayMap、ChannelMAXICMap 和 DataTable 的频繁分配。
-
优化:
-
重用 m_DisplayMap 的 List<double>,避免重复创建。
-
使用静态 DataTable 或缓存,减少分配。
-
-
代码:
csharp
private void InitDataMap() { m_DisplayMap.Clear(); // 重用现有 Dictionary if (Name.Contains("GBPS")) { m_DisplayMap["VGETIME"] = new List<double>(); m_DisplayMap["VGEPSRealV"] = new List<double>(); } else if (Name.Contains("RBPS")) { m_DisplayMap["VCETIME"] = new List<double>(); m_DisplayMap["VCEPSRealV"] = new List<double>(); } } private DataTable stopDataTableCache = null; private DataTable CreateStopDatatable() { if (stopDataTableCache == null) { stopDataTableCache = new DataTable(); stopDataTableCache.Columns.Add("ParaName", typeof(string)); stopDataTableCache.Columns.Add("ParaValue", typeof(string)); stopDataTableCache.Columns.Add("Unit", typeof(string)); stopDataTableCache.Columns.Add("InputType", typeof(string)); stopDataTableCache.Columns.Add("DataBase", typeof(string)); } stopDataTableCache.Rows.Clear(); stopDataTableCache.AcceptChanges(); stopDataTableCache.Rows.Add("PowerSupplyID", Name, "", "ComboBox", ""); stopDataTableCache.Rows.Add("DUTStartV", "0", "V", "NumberUpDown", ""); stopDataTableCache.Rows.Add("DUTStopV", "0", "V", "NumberUpDown", ""); stopDataTableCache.Rows.Add("Steps", "1", "", "NumberUpDown", ""); stopDataTableCache.Rows.Add("StabilizingTime", "1", "s", "NumberUpDown", ""); stopDataTableCache.Rows.Add("MaxI", ParameterMap["MaxI"].Upper.ToString(), "A", "NumberUpDown", ""); return stopDataTableCache; } -
解释:
-
重用 m_DisplayMap 和 stopDataTableCache,减少 Dictionary 和 DataTable 分配。
-
清空 List<double> 而非创建新实例,降低 GC 压力。
-
- 优化 GDI 句柄(假设 UI 交互)
-
问题:SystemNotifEvent.HardInfoNotifEvent 可能触发 UI 更新。
-
优化:
- 使用批量更新或检查控件状态。
-
代码(假设在 UpdataMonitorData 中):
csharp
private void UpdataMonitorData() { if (InvokeRequired) { Invoke((MethodInvoker)UpdataMonitorData); return; } try { var testSection = SelectControlTestUnitSelecter.TestSection; if (testSection == null) return; double progress = testSection.GetPrograss(); if (labelPercent.Text != $"{progress:F1}%") labelPercent.Text = $"{progress:F1}%"; // 避免重复设置 double timeLeft = testSection.GetTimeLeft(); if (labelTimeLeft.Text != $"{timeLeft:F0} min") labelTimeLeft.Text = $"{timeLeft:F0} min"; ChangeChartView(); } catch (Exception ex) { Log($"UpdataMonitorData 错误: {ex.Message}", false); } }
- 优化日志和文件句柄
-
问题:log4net 高频写入可能导致文件句柄泄漏。
-
优化:
-
配置 log4net 限制文件大小和备份。
-
log4net.config:
xml
<log4net> <appender name="FileAppender" type="log4net.Appender.RollingFileAppender"> <file value="app_log.txt" /> <appendToFile value="true" /> <rollingStyle value="Size" /> <maxSizeRollBackups value="5" /> <maximumFileSize value="10MB" /> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %message%newline" /> </layout> </appender> <root> <level value="INFO" /> <appender-ref ref="FileAppender" /> </root> </log4net> -
确保程序退出时关闭 log4net:
csharp
protected override void OnFormClosing(FormClosingEventArgs e) { base.OnFormClosing(e); LogManager.Shutdown(); timerManager?.Dispose(); SelectControlTestUnitSelecter.TestSection?.Dispose(); }
-
- 优化非托管资源
-
问题:m_Hardware 未始终正确释放。
-
优化:
-
确保 Uninitialize 关闭会话。
-
假设 IHardwareAdaptor 实现 IDisposable:
csharp
public void Uninitialize() { try { if (m_Hardware == null || !m_Hardware.Connected) return; SystemNotifEvent.HardInfoNotifEvent(this, Name + ": 正在进行停止卸载操作。", false); m_Hardware.CloseSession(); m_Hardware.UnInitialize(); if (m_Hardware is IDisposable disposable) disposable.Dispose(); } catch (Exception ex) { m_Log.Error($"{Name}: Uninitialize 失败 - {ex.Message}"); } finally { HardwareConnectStatus = HardwareConnectStatus.Idle; m_Hardware = null; } }
-
三、检测与验证
- 使用工具检测
-
Visual Studio 诊断工具:
-
捕获内存快照,检查 PowerSupply 实例数和 m_DisplayMap 的 List<double> 大小。
-
监控句柄计数(Process.HandleCount),确认线程句柄是否增加。
-
-
dotMemory:
-
分析 PowerSupply 是否被 PowerError 事件持有。
-
检查 DataTable 和 Dictionary 分配频率。
-
-
perfmon:
- 监控 Handle Count 和 Thread Count,确认 StartQuery 是否导致线程泄漏。
-
GDIView:
- 检查 UpdataMonitorData 是否增加 GDI 句柄。
-
Process Explorer:
- 查看线程堆栈,定位 StartQuery 和 StopPower 的线程。
- 日志监控
-
代码:
csharp
private int updateCount = 0; private DateTime lastLogTime = DateTime.MinValue; private readonly TimeSpan logInterval = TimeSpan.FromMinutes(1); private void Log(string message, bool isSummary) { try { updateCount++; string logMessage = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss}: {message} (调用次数: {updateCount})"; if (isSummary || DateTime.Now - lastLogTime >= logInterval) { logMessage += $"\nGC 统计: Gen0={GC.CollectionCount(0)}, Gen1={GC.CollectionCount(1)}, Gen2={GC.CollectionCount(2)}"; logMessage += $"\n内存使用: 托管堆={GC.GetTotalMemory(false) / 1024.0 / 1024.0:F2} MB"; logMessage += $"\n句柄计数: {System.Diagnostics.Process.GetCurrentProcess().HandleCount}"; logMessage += $"\n线程计数: {System.Diagnostics.Process.GetCurrentProcess().Threads.Count}"; lastLogTime = DateTime.Now; } m_Log.Info(logMessage); } catch { // 忽略日志写入错误 } } -
分析:
-
检查 app_log.txt,确认句柄计数是否稳定(200-1000)。
-
如果线程计数增加(>10),检查 StartQuery 调用频率。
-
PowerShell 脚本:
powershell
$logFile = "C:\path\to\app_log.txt" $lines = Get-Content $logFile $errorCount = ($lines | Select-String "错误").Count $handleCount = ($lines | Select-String "句柄计数" | Select-Object -Last 1).Line $threadCount = ($lines | Select-String "线程计数" | Select-Object -Last 1).Line Write-Output "错误次数: $errorCount" Write-Output "最新句柄计数: $handleCount" Write-Output "最新线程计数: $threadCount"
-
- 验证效果
-
运行 10-30 分钟,执行 StartTest、StopTest 和 StartQuery。
-
检查日志:
-
正常:句柄计数稳定,线程数 3-5,Gen0 回收 10-20 次/分钟。
-
泄漏:句柄计数 >1000,线程数 >20,Gen0 回收 >50 次/分钟。
-
四、中文解释
- PowerSupply 的内存泄漏风险
-
线程句柄:StartQuery 等方法频繁创建线程,未显式终止。
-
事件订阅:PowerError 未清理,可能导致 PowerSupply 被持有。
-
集合分配:m_DisplayMap 和 DataTable 重复创建,增加 GC 压力。
-
GDI 句柄:UI 更新(如 HardInfoNotifEvent)可能增加 GDI 句柄。
-
文件句柄:log4net 高频写入可能导致句柄泄漏。
-
非托管资源:m_Hardware 未始终正确释放。
- 优化效果
-
使用 CancellationToken 管理线程,减少句柄泄漏。
-
实现 IDisposable,清理事件和资源。
-
重用集合,降低 GC 压力。
-
配置 log4net 和 UI 更新,减少句柄分配。
- 检测方法
-
使用 Visual Studio、dotMemory、perfmon 和 GDIView 监控泄漏。
-
日志记录句柄和线程计数,自动化分析。
五、注意事项
-
测试验证:运行 10-30 分钟,检查日志和句柄计数。
-
IHardwareAdaptor:确认是否实现 IDisposable,否则需手动释放句柄。
-
升级框架:.NET Framework 4.0 性能有限,建议升级到 .NET Framework 4.8 或 .NET Core。
-
句柄限制:启用 /LARGEADDRESSAWARE:
bash
editbin /LARGEADDRESSAWARE ROBOT.exe
如需进一步优化(如 IHardwareAdaptor 的定义或 UI 交互逻辑),请提供相关代码,我将提供更精确的方案!