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

Blu*_*eft 15 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(..);
}
Run Code Online (Sandbox Code Playgroud)

然后别的,我打电话

//Some code..
await someClassInstance.WaitForThing();
//Some more code
Run Code Online (Sandbox Code Playgroud)

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

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

nos*_*tio 9

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

为什么GC在我引用它时会收集我的对象?

所以我将这个标记为社区维基.

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

如果调用上下文是指编译器生成的状态机对象(表示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);
    }
}
Run Code Online (Sandbox Code Playgroud)

输出:

Press enter to GC

~SomeDisposable
Press enter to exit

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

在编码系统级异步内容时,这可能会导致一些模糊的错误.GCHandle.Alloc(callback)在任何未引用外部async方法的OS互操作回调中使用非常重要.这样做GC.KeepAlive(callback)在年底单独async方法是无效的.我在这里详细介绍了这个:

异步/等待,自定义awaiter和垃圾收集器

另外,还有另一种C#状态机:一种方法return yield.有趣的是,与IEnumerableor一起IEnumerator,它也实现了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"
Run Code Online (Sandbox Code Playgroud)

与此不同,用async的方法有控制的unwiding的没有直接的方法usingfinally.


Ste*_*ary 6

您应确保始终完成任务.

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

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