gpt4 book ai didi

c# - 在后台线程中创建可卡住对象时发生资源泄漏

转载 作者:行者123 更新时间:2023-11-30 16:52:49 24 4
gpt4 key购买 nike

在我的应用程序中,我创建了 Freezable后台(线程池)线程中的对象,卡住它们,然后在主线程上显示它们。一切正常,除了一段时间后,整个系统变得迟缓并且应用程序最终崩溃。

我已经设法将问题减少到这一行:

var temp = new DrawingGroup();

如果您在不同的后台(非 UI)线程上运行的次数足够多,整个系统就会变得迟缓,最终应用程序会崩溃。

(在我的实际应用程序中,我然后向该对象绘制一些东西,将其卡住并稍后在主线程上显示它,但这不是重现问题所必需的。)

重现该问题的完整代码(复制到默认的空白 wpf 应用程序):

public partial class MainWindow : Window
{
private DispatcherTimer dt;

public MainWindow()
{
InitializeComponent();

dt = new DispatcherTimer();
dt.Interval = TimeSpan.FromSeconds(0.1);
dt.Tick += dt_Tick;
dt.IsEnabled = true;
}

private int counter = 0;
void dt_Tick(object sender, EventArgs e)
{
for (int i = 0; i < 100; i++)
{
var thread = new Thread(MemoryLeakTest);
thread.Start();
}

Title = string.Format("Mem leak test {0}", counter++);

}

private void MemoryLeakTest()
{
try
{
var temp = new DrawingGroup();
temp.Freeze();
}
catch (Exception e)
{
dt.IsEnabled = false;
MessageBox.Show(e.Message+Environment.NewLine+e.StackTrace);
}
}
}

在约 150 次计时器运行后(即在短时间内创建了大约 15000 个线程后),我得到这个异常:

Not enough storage is available to process this command
bei MS.Win32.HwndWrapper..ctor(Int32 classStyle, Int32 style, Int32 exStyle, Int32 x, Int32 y, Int32 width, Int32 height, String name, IntPtr parent, HwndWrapperHook[] hooks)
bei System.Windows.Threading.Dispatcher..ctor()
bei System.Windows.DependencyObject..ctor()
bei System.Windows.Media.DrawingGroup..ctor()
bei WpfApplication5.MainWindow.MemoryLeakTest() in ...

我认为发生的事情是这样的:

  1. DrawingGroup源自 DependencyObject , 和 DependencyObject的构造函数使用 Dispatcher.CurrentDispatcher , 然后创建一个新的 Dispatcher对于这个线程。
  2. 新的调度程序分配了一些 Win32 资源。
  3. 查看 HwndWrapper我认为 Reflector 中的最终代码 HwndWrapper尝试使用 Dispatcher.BeginInvoke 同步它自己的清理.
  4. 由于此后台线程从不启动消息循环,因此永远不会调用清理代码=>资源泄漏

有没有办法解决或解决这个问题?

到目前为止我尝试了什么:

  • 显然,使用 ThreadPoolTasks而不是手动创建线程会延迟这个问题。但是 ThreadPool随着时间的推移也会创建和关闭新线程,这样只会延迟问题,而不是解决方案。
  • 在每个线程结束时强制进行完整的 GC 收集并没有改变任何东西。这与垃圾收集不确定性无关。
  • 调用 Dispatcher.InvokeShutdown在后台线程结束时手动似乎可以工作,但我看不出如何确保在每个 ThreadPool 结束时调用它线。不用自己写 ThreadPool ,也就是……

最佳答案

这是 .NET 中 Dispatcher 系统设计的一个已知缺陷。它会影响依赖于 Dispatcher 的 WPF 和非 WPF 库。 Microsoft 已表示不会修复此问题。

它与卡住操作或任何操作的使用无关。从 DependencyObject 派生的任何对象(类)都将有一个基本构造函数,它会触发 Dispatcher 的创建> 该线程的实例,如果它之前没有被创建的话。换句话说,Dispatcher 在设计上是一个线程本地单例。

当足够(数万)个 Dispatcher 实例被泄露时,就会发生崩溃。这意味着在应用程序的生命周期中创建和销毁了相同数量的线程,每个线程都创建了一个或多个 DependencyObject。询问任何应用程序开发人员,他们都会说不常见,虽然本身还不错,但绝对需要特别注意来设计具有多个线程的应用程序已被创建和销毁。


在开始之前,这是一种查询 Dispatcher 的安全方法,如果它之前不存在,则不会导致自动创建.

Thread currentThread = Thread.CurrentThread;
Dispatcher currentDispatcherOrNull = Dispatcher.FromThread(currentThread);

MSDN: Dispatcher.FromThread method


首先,您可以在完成线程后关闭调度程序。

MSDN: Dispatcher.InvokeShutdown method


其次,意识到一旦为一个线程 关闭,就不可能为同一线程重新初始化Dispatcher .换句话说,在 InvokeShutdown 之后,无法在该线程上使用 WPF 或依赖于 Dispatcher 的任何其他库。线程实际上被毒死了。


结合第一点和第二点可以得出结论,您需要自己的线程池,每个线程池都被赋予一个Dispatcher。只要您能控制线程池的风速下降,就不会有泄漏的危险。


有一些流行的开源 .NET 线程池库可以与 .NET 系统线程池一起(独立于)运行。这是解决这个特定平台问题的合适方法。


如果您同时控制前端(表示层)和后端(图像渲染),则有一个更简单、严格且有效 (虽然未充分利用)方法:

  • 制定调用者必须初始化 Dispatcher 的策略;后端只会检查调度程序已经存在(通过 Dispatcher.FromThread),并且拒绝执行工作如果不是。

这种方法将负担转移到表示层,具有讽刺意味的是,它往往已经初始化了 Dispatcher。

这种方法也适用于一个线程池

关于c# - 在后台线程中创建可卡住对象时发生资源泄漏,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32137811/

24 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com