从未完成的任务会怎样?他们是否妥善处置?

What happens to Tasks that are never completed? Are they properly disposed?

假设我有以下 class:

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 自动神奇地知道需要处理调用上下文?

You should ensure your tasks are always completed.

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

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

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

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

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

However, let's say SetResult(..) is never called, and someClassInstance stops being referenced and is garbage collected. Does this create a memory leak? Or does .Net auto-magically know the calling-context needs to be disposed?

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

输出:

Press enter to GC

~SomeDisposable
Press enter to exit

IMO,这种行为是合乎逻辑的,但尽管 using 的范围从未结束(因此它的 SomeDisposable.Dispose 从未被调用过)并且 TestSomethingAsync 返回的 Task 仍然存在并在 Main.

中被引用

这可能会导致在编写系统级异步内容时出现一些不明显的错误。在 async 方法之外未引用的任何 OS 互操作回调上使用 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.

的展开