问题
我昨天刚刚输入了一些性能记录,因为我注意到很久以前观看任务管理器的句柄泄漏,尽管修复它一直是低优先级。这是隔夜运行,每 10 秒采样一次。
由于时间限制,我还没有将它运行失败,而且我的测试计算机也是我的开发计算机,所以在编写代码时运行它并不理想......所以我不确定它是否/何时会崩溃,但我高度怀疑这只是时间问题。
注意:区域中的红色框是我“停止”工作循环并在短暂暂停后重新启动它的地方。线程在“停止”时从 ~100 下降到 ~20。直到循环从 ~62,000 到 ~40,000 大约 30 秒后重新启动,Handles 才下降。所以一些句柄正在被 GC 处理,只是没有我预期的那么多。我无法弄清楚是什么根阻止了所有这些句柄被收集或它们最初来自哪里(即任务、GUI、文件等)。
如果您已经知道可能导致此问题的原因,则无需进一步阅读。我已经提供了其余的信息和代码,以供参考,以霰弹枪式的方法解决问题。随着根本原因的缩小,我将删除、编辑等。同样,如果缺少感兴趣的内容,请告诉我,我会尽力提供(日志、转储等)。
我做了什么
就我自己而言,我已经完成了关于跟踪句柄滥用的本教程,并查看了转储文件以找到句柄打开和关闭的位置......但是它对于成千上万的句柄来说实在是太难以理解了而且我在加载符号时遇到了麻烦,所以指针对我来说只是胡言乱语。
我还没有通过我的列表中的以下两个,但想知道是否有一些更友好的方法首先......
我还将我怀疑可能导致此问题的代码拆分到另一个小型应用程序中,并且所有内容似乎都可以毫无问题地收集垃圾(尽管与实际应用程序相比,执行模式大大简化了)。
潜在的罪魁祸首
我确实有几个长期存在的实例类,只要应用程序打开,它们就会持续存在,包括 5 个表单,每个表单只创建一次,然后根据需要隐藏/显示。我使用一个主对象作为我的应用程序控制器,然后模型和视图通过事件以演示者优先模式连接到演示者。
以下是我在此应用程序中所做的一些事情,它们可能重要也可能不重要:
- 广泛使用 custom
Action
和Func
lambdas,其中一些可能是长期存在的 - 3 个用于事件的自定义委托,它们可以生成
Task
用于异步执行的 s。 - 安全调用的扩展
Controls
。 - 非常非常大量地使用
Task
和Parallel.For
/Parallel.Foreach
来运行工作方法(或上面提到的事件) - 永远不要使用 Thread.Sleep(),而是使用 AutoResetEvent 的自定义 Sleep.For()。
主循环
此应用程序运行时的一般流程基于离线版本中一系列文件的循环和在线版本中数字输入信号的轮询。下面是带有离线版本注释的 sudo 代码,这是我可以在笔记本电脑上运行而无需外部硬件以及上面的图表正在监控的内容(我目前无法访问在线模式的硬件)。
public void foo()
{
// Sudo Code
var InfiniteReplay = true;
var Stopped = new CancellationToken();
var FileList = new List<string>();
var AutoMode = new ManualResetEvent(false);
var CompleteSignal = new ManualResetEvent(false);
Action<CancellationToken> PauseIfRequired = (tkn) => { };
// Enumerate a Directory...
// ... Load each file and do work
do
{
foreach (var File in FileList)
{
/// Method stops the loop waiting on a local AutoResetEvent
/// if the CompleteSignal returns faster than the
/// desired working rate of ~2 seconds
PauseIfRequired(Stopped);
/// While not 'Stopped', poll for Automatic Mode
/// NOTE: This mimics how the online system polls a digital
/// input instead of a ManualResetEvent.
while (!Stopped.IsCancellationRequested)
{
if (AutoMode.WaitOne(100))
{
/// Class level Field as the Interface did not allow
/// for passing the string with the event below
m_nextFile = File;
// Raises Event async using Task.Factory.StartNew() extension
m_acquireData.Raise();
break;
}
}
// Escape if Canceled
if (Stopped.IsCancellationRequested)
break;
// If In Automatic Mode, Wait for Complete Signal
if (AutoMode.WaitOne(0))
{
// Ensure Signal Transition
CompleteSignal.WaitOne(0);
if (!CompleteSignal.WaitOne(10000))
{
// Log timeout and warn User after 10 seconds, then continue looping
}
}
}
// Keep looping through same set of files until 'Stopped' if in Infinite Replay Mode
} while (!Stopped.IsCancellationRequested && InfiniteReplay);
}
异步事件
下面是事件的扩展,大多数是使用默认的异步选项执行的。'TryRaising()' 扩展只是将委托包装在 try-catch 中并记录任何异常(虽然它们不会重新抛出,但它们不是负责捕获异常的正常程序流程的一部分)。
using System.Threading.Tasks;
using System;
namespace Common.EventDelegates
{
public delegate void TriggerEvent();
public delegate void ValueEvent<T>(T p_value) where T : struct;
public delegate void ReferenceEvent<T>(T p_reference);
public static partial class DelegateExtensions
{
public static void Raise(this TriggerEvent p_response, bool p_synchronized = false)
{
if (p_response == null)
return;
if (!p_synchronized)
Task.Factory.StartNew(() => { p_response.TryRaising(); });
else
p_response.TryRaising();
}
public static void Broadcast<T>(this ValueEvent<T> p_response, T p_value, bool p_synchronized = false)
where T : struct
{
if (p_response == null)
return;
if (!p_synchronized)
Task.Factory.StartNew(() => { p_response.TryBroadcasting(p_value); });
else
p_response.TryBroadcasting(p_value);
}
public static void Send<T>(this ReferenceEvent<T> p_response, T p_reference, bool p_synchronized = false)
where T : class
{
if (p_response == null)
return;
if (!p_synchronized)
Task.Factory.StartNew(() => { p_response.TrySending(p_reference); });
else
p_response.TrySending(p_reference);
}
}
}
GUI 安全调用
using System;
using System.Windows.Forms;
using Common.FluentValidation;
using Common.Environment;
namespace Common.Extensions
{
public static class InvokeExtensions
{
/// <summary>
/// Execute a method on the control's owning thread.
/// </summary>
/// http://stackoverflow.com/q/714666
public static void SafeInvoke(this Control p_control, Action p_action, bool p_forceSynchronous = false)
{
p_control
.CannotBeNull("p_control");
if (p_control.InvokeRequired)
{
if (p_forceSynchronous)
p_control.Invoke((Action)delegate { SafeInvoke(p_control, p_action, p_forceSynchronous); });
else
p_control.BeginInvoke((Action)delegate { SafeInvoke(p_control, p_action, p_forceSynchronous); });
}
else
{
if (!p_control.IsHandleCreated)
{
// The user is responsible for ensuring that the control has a valid handle
throw
new
InvalidOperationException("SafeInvoke on \"" + p_control.Name + "\" failed because the control had no handle.");
/// jwdebug
/// Only manually create handles when knowingly on the GUI thread
/// Add the line below to generate a handle http://stackoverflow.com/a/3289692/1718702
//var h = this.Handle;
}
if (p_control.IsDisposed)
throw
new
ObjectDisposedException("Control is already disposed.");
p_action.Invoke();
}
}
}
}
睡眠.For()
using System.Threading;
using Common.FluentValidation;
namespace Common.Environment
{
public static partial class Sleep
{
public static bool For(int p_milliseconds, CancellationToken p_cancelToken = default(CancellationToken))
{
// Used as "No-Op" during debug
if (p_milliseconds == 0)
return false;
// Validate
p_milliseconds
.MustBeEqualOrAbove(0, "p_milliseconds");
// Exit immediate if cancelled
if (p_cancelToken != default(CancellationToken))
if (p_cancelToken.IsCancellationRequested)
return true;
var SleepTimer =
new AutoResetEvent(false);
// Cancellation Callback Action
if (p_cancelToken != default(CancellationToken))
p_cancelToken
.Register(() => SleepTimer.Set());
// Block on SleepTimer
var Canceled = SleepTimer.WaitOne(p_milliseconds);
return Canceled;
}
}
}