【问题标题】:Can TaskCompletionSource that never complete cause memory leaks?从未完成的 TaskCompletionSource 会导致内存泄漏吗?
【发布时间】:2021-12-03 04:51:53
【问题描述】:

我有一个组件,它可以执行长时间的异步操作,并且可以在该操作完成之前被销毁。在这种情况下,我只想停止运行这些操作并忘记它们,没有任何例外。我有下一个例子:

private CancellationTokenSource destroyTokenSource = new CancellationTokenSource();

private async void RunExample()
{
    await LongAction().MuteOnDestroy(destroyTokenSource.Token);
    // ... actions that should not be performed with destroyed object
}

private async Task<bool> LongAction()
{
    // some very long actions
    return true;
}

// ...

public static class TaskExtension
{
    public static Task<T> MuteOnDestroy<T>(this Task<T> task, CancellationToken destroyToken)
    {
        var promise = new TaskCompletionSource<T>();
        task.ContinueWith(t =>
        {
            if (!destroyToken.IsCancellationRequested)
            {
                switch (t.Status)
                {
                    case TaskStatus.Canceled:
                        promise.SetCanceled();
                        break;
                    case TaskStatus.Faulted:
                        promise.SetException(t.Exception!);
                        break;
                    case TaskStatus.RanToCompletion:
                        promise.SetResult(t.Result);
                        break;
                }
            }
        }, TaskContinuationOptions.ExecuteSynchronously);
        return promise.Task;
    }
}

我假设永远不会完成的异步方法(在此示例中为 RunExample 方法)(destroyToken.IsCancellationRequested == true)会导致内存泄漏,但我不确定,因为我不知道“永远等待”的延续在哪里" 方法被存储。

会导致内存泄漏吗?

【问题讨论】:

  • async void 用于事件处理程序。 RunExample 方法是事件处理程序吗?如果没有,是否可以将其从async void 转换为async Task
  • @TheodorZoulias,是的,有可能
  • 如果您有办法管理并最终 await 这些任务,我可能值得切换到 async Task。否则,如果您想以一种即发即弃的方式启动这些任务,则最好坚持使用async void。至少在出现错误的情况下,应用程序会崩溃而不是挂起。

标签: c# .net async-await task


【解决方案1】:

是的,您绝对会导致泄漏。 TPL 将确保 GC 不会清理任何正在进行的任务,并且所有这些任务都将引用其延续委托,而这些委托本身通常会引用更多对象。如果这些对象被允许被 GC 清理,那将是一个问题,因为这是所有可以并且很可能最终会运行的代码。 GC 的全部意义在于它不会清除对可以从可以执行的代码中访问的对象的引用。

问题在于,如果取消令牌在您包装的任务完成之前被取消,那么您使用取消令牌包装任务的尝试永远不会提前完成。它一直等到操作正常完成,才将包装的任务标记为已取消。

幸运的是,正确解决这个问题实际上是simpler than your existing solution,因为您可以利用现有的 TPL 工具来完成这种情况下的大部分工作。

链接的解决方案将导致任务被更快地标记为已取消,因此长时间运行的任务本身仍将保持对其使用的所有对象的引用直到它完成,但利用取消令牌的延续将能够尽快取消并释放他们持有的任何资源。

【讨论】:

  • 在我的环境(Unity)中,组件可以在长任务完成之前被销毁,我不想在任务完成后捕获取消的异常或检查组件的状态。我只想取消所有的继续并忘记。
  • @Dem0n13 好吧,您有两个选择,您可以使用建议的代码尽快取消任务,让延续在取消时执行适合他们的任何行为,或者您可以使用您的代码并让活动任务保持不变,并将每个 Task 对象和每个对象保留在任务或延续的引用树中。如果您不确定哪个对您的应用程序来说更成问题,请务必对两者进行一些性能测试,以查看消耗了多少内存以及它是否对您来说是个问题。
猜你喜欢
  • 2021-03-23
  • 2021-09-25
  • 2014-12-08
  • 1970-01-01
  • 1970-01-01
  • 2010-09-09
  • 2011-06-16
  • 2011-10-28
相关资源
最近更新 更多