【问题标题】:What happens to Tasks that are never completed? Are they properly disposed?从未完成的任务会发生什么?它们是否妥善处置?
【发布时间】:2015-02-02 12:59:50
【问题描述】:

假设我有以下课程:

class SomeClass
{
    private TaskCompletionSource<string> _someTask;

    public Task<string> WaitForThing()
    {
        _someTask = new TaskCompletionSource<string>();
        return _someTask.Task;
    }

    //Other code which calls _someTask.SetResult(..);
}

然后在别处,我打电话

//Some code..
await someClassInstance.WaitForThing();
//Some more code

在调用_someTask.SetResult(..) 之前,不会调用//Some more code。调用上下文在内存中某处等待。

但是,假设 SetResult(..) 从未被调用,someClassInstance 停止被引用并被垃圾回收。这会造成内存泄漏吗?或者 .Net 是否自动神奇地知道需要处理调用上下文?

【问题讨论】:

  • TCS 是一个孤立的对象。当它无法访问时,它会被收集。它的任务不引用 TCS,因此您可以在内存中没有任何相应的 TCS 的情况下拥有无法完成的任务。如果该任务也未被引用,那么它也会被收集。

标签: c# task-parallel-library async-await


【解决方案1】:

更新,@SriramSakthivel 的一个好观点,原来我已经回答了一个非常相似的问题:

Why does GC collects my object when I have a reference to it?

所以我将此标记为社区 wiki。

但是,假设 SetResult(..) 从未被调用,并且 someClassInstance 停止被引用并被垃圾收集。 这会造成内存泄漏吗?或者.Net是否自动神奇地知道 call-context 需要被释放吗?

如果 calling-context 是指编译器生成的状态机对象(代表async 方法的状态),那么是的,它确实会完成。

示例:

static void Main(string[] args)
{
    var task = TestSomethingAsync();
    Console.WriteLine("Press enter to GC");
    Console.ReadLine();
    GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, true);
    GC.WaitForFullGCComplete();
    GC.WaitForPendingFinalizers();
    Console.WriteLine("Press enter to exit");
    Console.ReadLine();
}

static async Task TestSomethingAsync()
{
    using (var something = new SomeDisposable())
    {
        await something.WaitForThingAsync();
    }
}

class SomeDisposable : IDisposable
{
    readonly TaskCompletionSource<string> _tcs = new TaskCompletionSource<string>();

    ~SomeDisposable()
    {
        Console.WriteLine("~SomeDisposable");
    }

    public Task<string> WaitForThingAsync()
    {
        return _tcs.Task;
    }

    public void Dispose()
    {
        Console.WriteLine("SomeDisposable.Dispose");
        GC.SuppressFinalize(this);
    }
}

输出:

按回车键进入 GC 〜一些一次性 按回车退出

IMO,这种行为是合乎逻辑的,但尽管using 的作用域从未结束(因此它的SomeDisposable.Dispose 从未被调用过),但something 最终确定仍然可能有点意外并且 TestSomethingAsync 返回的 Task 仍然存在并且在 Main 中引用。

在编写系统级异步代码时,这可能会导致一些难以理解的错误。在async 方法之外未引用的任何操作系统互操作回调上使用GCHandle.Alloc(callback) 非常重要。在async方法的最后单独做GC.KeepAlive(callback)是无效的。我在这里详细写了这个:

Async/await, custom awaiter and garbage collector

附带说明,还有另一种 C# 状态机:带有return yield 的方法。有趣的是,与IEnumerableIEnumerator 一起,它还实现了IDisposable。调用其Dispose 将展开任何usingfinally 语句(即使在不完整的可枚举序列的情况下):

static IEnumerator SomethingEnumerable()
{
    using (var disposable = new SomeDisposable())
    {
        try
        {
            Console.WriteLine("Step 1");
            yield return null;
            Console.WriteLine("Step 2");
            yield return null;
            Console.WriteLine("Step 3");
            yield return null;
        }
        finally
        {
            Console.WriteLine("Finally");
        }
    }
}
// ...
var something = SomethingEnumerable();
something.MoveNext(); // prints "Step 1"
var disposable = (IDisposable)something;
disposable.Dispose(); // prints "Finally", "SomeDisposable.Dispose"

与此不同,async 方法无法直接控制 usingfinally 的展开。

【讨论】:

  • 你怎么知道“编译器生成的状态机对象”保证在对象完成时被释放?
  • @BlueRaja-DannyPflughoeft,这里的“被处置”是什么意思?状态机对象没有实现IDisposable。它只是一个装箱的struct,但它的所有字段(如上面的something)都会被垃圾收集和最终确定(注意~SomeDisposable 是如何被调用的)——因为结构本身会被GC'ed。
  • @SriramSakthivel,感谢您关于重复的观点,我已将此标记为 wiki。
【解决方案2】:

You should ensure your tasks are always completed.

在通常情况下,“调用 SetResult 的其他代码”在某处注册为回调。例如,如果它使用非托管的重叠 I/O,那么该回调方法就是一个 GC 根。然后,该回调显式地保持_someTask 处于活动状态,从而保持其Task 处于活动状态,从而使//Some more code 的委托保持活动状态。

如果“调用 SetResult 的其他代码”没有(直接或间接)注册为回调,那么我不认为会有泄漏。请注意,这不是受支持的用例,因此无法保证。但我确实使用您问题中的代码创建了内存分析测试,它似乎没有泄漏。

【讨论】:

  • 所以这取决于“其他代码”在哪里?我对如何判断哪些情况会泄漏感到困惑......我的用例是WaitForThing() 等待来自 IM 客户端另一端用户的用户输入 (并从对象调用代表一个“朋友”)并返回它。但是,如果该用户被删除为朋友,则该输入永远不会发生 - 我无法“完成”它。
  • @BlueRaja-DannyPflughoeft:当用户被删除为好友时,我建议您完成已取消的任务 (SetCanceled)。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-13
  • 2021-12-06
  • 2013-11-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多