【问题标题】:Task await and thread terminate are causing memory leak任务等待和线程终止导致内存泄漏
【发布时间】:2018-09-11 11:25:42
【问题描述】:

我正在我的窗口中执行异步任务:

private async Task LoadData()
{
    // Fetch data
    var data = await FetchData();

    // Display data
    // ...
}

窗口在单独的线程中启动:

// Create and run new thread
var thread = new Thread(ThreadMain);
thread.SetApartmentState(ApartmentState.STA);
thread.IsBackground = true;
thread.CurrentCulture = Thread.CurrentThread.CurrentCulture;
thread.CurrentUICulture = Thread.CurrentThread.CurrentUICulture;
thread.Start();

private static void ThreadMain()
{
    // Set synchronization context
    var dispatcher = Dispatcher.CurrentDispatcher;
    SynchronizationContext.SetSynchronizationContext(
        new DispatcherSynchronizationContext(dispatcher));

    // Show window
    var window = new MyWindow();
    window.ShowDialog();

    // Shutdown
    dispatcher.InvokeShutdown();
}

当windows关闭时,线程当然结束了,这样就好了。

现在 - 如果在 FetchData 完成之前发生这种情况,我会得到内存泄漏。

似乎FetchData 一直在等待,而Task.s_currentActiveTasks 静态字段将永远保留我的窗口实例:

保留路径

System.Collections.Generic.Dictionary.entries -> System.Collections.Generic.Dictionary+Entry[37] 在 [17].值-> System.Threading.Tasks.Task.m_continuationObject -> System.Threading.Tasks.SynchronizationContextAwaitTaskContinuation.m_action -> System.Action._target -> System.Runtime.CompilerServices.AsyncMethodBuilderCore+ContinuationWrapper.m_continuation -> System.Action._target -> System.Runtime.CompilerServices.AsyncMethodBuilderCore+ContinuationWrapper.m_continuation -> System.Action._target -> ...

如果我理解正确,如果/当 FetchData 完成时,继续应该在窗口实例目标和线程上继续,但自从线程完成后就不会发生这种情况。

有什么解决办法,这种情况下如何避免内存泄漏?

【问题讨论】:

  • “窗口在单独的线程中启动” - 为什么?你永远不需要它,async/await 让避免它变得更容易。
  • @HenkHolterman 因为用户需要能够使用不同的功能,而不需要每个功能都通过模态对话框阻塞所有其他功能。
  • 所以你使用线程将.ShowDialog() 还原为.Show() ?
  • 认为这里的真正问题是 - 当等待的任务由于原始线程/同步上下文不复存在而无法恢复时,会发生什么情况?
  • 您的第一个错误是创建另一个 sta 线程并将其用于任何 UI。不要那样做。这是一个非常糟糕的主意,它会导致更多值得的问题。

标签: c# wpf multithreading async-await


【解决方案1】:

我认为您无法解决这个问题(我的意思是在不改变整体设计的情况下)。使用 await 和 available SynchronizationContext,将继续(在 await 之后)发布到该上下文。此延续包括完成生成任务的代码。所以在你的例子中:

private async Task LoadData()
{
    var data = await FetchData();
    // the rest is posted to sync context
    // and only when the rest is finished
    // task returned from LoadData goes to completed state         
} 

发布到 WPF 同步上下文与在调度程序上执行 BeginInvoke 相同。但是,在已关闭的调度程序上执行BeginInvoke 不会引发异常并返回DispatcherOperation,状态为DispatcherOperationStatus.Aborted。当然,你传递给BeginInvoke的委托在这种情况下不会被执行。

所以结果 - 你的 LoadData 的延续被传递给关闭调度程序,它默默地忽略它(嗯,不是默默地 - 它返回 Aborted 状态,但没有办法观察它,因为 SynchronizationContext.Post 具有返回类型 @ 987654332@)。由于 continuation 永远不会执行 - 从 LoadData 返回的任务永远不会完成\fails\cancels 并且始终处于运行状态。

您可以通过提供自己的同步上下文来验证它,看看它是如何进行的:

internal class SimpleDispatcherContext : SynchronizationContext
{
    private readonly Dispatcher _dispatcher;
    private readonly DispatcherPriority _priority;
    public SimpleDispatcherContext(Dispatcher dispatcher, DispatcherPriority priority = DispatcherPriority.Normal)
    {
        _dispatcher = dispatcher;
        _priority = priority;
    }

    public override void Post(SendOrPostCallback d, object state) {
        var op = this._dispatcher.BeginInvoke(_priority, d, state);
        // here, default sync context just does nothing
        if (op.Status == DispatcherOperationStatus.Aborted)
            throw new OperationCanceledException("Dispatcher has shut down");
    }

    public override void Send(SendOrPostCallback d, object state) {
        _dispatcher.Invoke(d, _priority, state);
    }
}

private async Task LoadData()
{
    SynchronizationContext.SetSynchronizationContext(new SimpleDispatcherContext(Dispatcher));
    // Fetch data
    var data = await FetchData();

    // Display data
    // ...
}

因此,您可以忍受这种情况(如果您认为这是一个轻微的泄漏 - 因为用户应该真正打开多少个窗口才能产生明显的效果,数千个?)或者以某种方式跟踪您的待处理操作并阻止关闭窗口,直到它们全部解决(或允许关闭窗口但阻止调度程序关闭,直到解决所有待处理的操作)。

【讨论】:

  • 我还没有验证这一点,但我希望 TaskScheduler.UnobservedTaskException 在这种情况下会因为失败的继续而被解雇,尽管不是在 GC 发生之前。我在here 上写了更多内容。
  • @Noseratio 我不这么认为。在这种情况下,不会抛出异常,只是不执行异步方法的延续(被关闭调度程序忽略)。所以我希望目标任务处于运行状态。例如,您的链接任务出现故障但未观察到。
猜你喜欢
  • 2019-07-16
  • 1970-01-01
  • 2011-09-23
  • 2021-02-25
  • 1970-01-01
  • 1970-01-01
  • 2014-07-17
  • 1970-01-01
  • 2022-11-06
相关资源
最近更新 更多