从未完成的任务会发生什么?他们妥善处理?

时间:2015-01-31 22:11:05

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

说我有以下课程:

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

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

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

2 个答案:

答案 0 :(得分:9)

更新了,@ SriramSakthivel提出了一个很好的观点,事实证明我已经回答了一个非常类似的问题:

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

所以我将此标记为社区维基。

  

然而,让我们说SetResult(..)永远不会被调用,而且   someClassInstance停止被引用并被垃圾收集。   这会造成内存泄漏吗?或者.Net自动神奇地知道了   呼叫上下文需要处理吗?

如果通过调用上下文表示编译器生成的状态机对象(表示async方法的状态),那么是的,它确实将被最终确定。< / p>

示例:

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);
    }
}

<强>输出:

Press enter to GC

~SomeDisposable
Press enter to exit

IMO,这种行为是合乎逻辑的,但something尽管using范围从未结束(因此SomeDisposable.Dispose TaskTestSomethingAsync仍然可能有点出乎意料。 } {从未被调用过)并且Main返回的GCHandle.Alloc(callback)仍然有效且在async中被引用。

在编码系统级异步内容时,这可能会导致一些模糊的错误。在GC.KeepAlive(callback)方法之外未引用的任何OS互操作回调上使用async非常重要。在return yield方法结束时单独执行IEnumerable无效。我在这里详细描述了这个:

Async/await, custom awaiter and garbage collector

旁注,还有另一种C#状态机:IEnumerator的方法。有趣的是,与IDisposableDispose一起,它还实现了using。调用其finally将展开任何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语句(即使在可枚举序列不完整的情况下):

using

与此不同的是,使用finally方法无法直接控制{{1}}和{{1}}的解除。

答案 1 :(得分:6)

You should ensure your tasks are always completed

在通常情况下,&#34;其他代码调用SetResult&#34;在某处注册为回调。例如,如果它使用非托管重叠I / O,则该回调方法是GC根。然后该回调明确地使_someTask保持活动状态,这使其Task保持活动状态,从而使//Some more code的委托保持活动状态。

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