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

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

假设我有以下类(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 自动神奇地知道需要处理调用上下文?

最佳答案

已更新,@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, this behavior is logical, but it still might be a bit unexpected that something gets finalized despite the fact that the using scope for it has never ended (and hence its SomeDisposable.Dispose has never been called) and that the Task returned by TestSomethingAsync is still alive and referenced in Main.

This could lead to some obscure bugs when coding system-level asynchronous stuff. It's really important to use GCHandle.Alloc(callback) on any OS interop callbacks which are not referenced outside async methods. Doing GC.KeepAlive(callback) alone at the end of the async method is not effective. I wrote about this in details here:

Async/await, custom awaiter and garbage collector

On a side note, there's another kind of C# state machine: a method with return yield. Interestingly, along with IEnumerable or IEnumerator, it also implements IDisposable. Invoking its Dispose will unwind any using and finally statements (even in the case of incomplete enumerable sequence):

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 的展开。

关于c# - 从未完成的任务会怎样?他们是否妥善处置?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28257095/

相关文章:

c# - 我如何重构这些函数以促进代码重用?

c# - 递归任务队列

c# - Task.Delay 会导致线程切换吗?

c# - 计算 Web 应用程序的在线用户数

c# - 仅打印具有来自数据 GridView 的值的行中的列

c# - 如何在 NotifyIcon 中使用 MouseWheel

c# - TPL .ContinueWith 在执行大量任务时优先

javascript - promise 不返还任何东西?

c# - 从异步方法调用 LINQ 查询的 ToList 而不是 ToListAsync 是否是一个潜在的死锁?

c# - 如果异步调用不一定在不同的线程上执行,那么阻塞异步调用如何会导致死锁?