Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Say I have the following class:

class SomeClass
{
    private TaskCompletionSource<string> _someTask;

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

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

Then elsewhere, I call

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

The //Some more code won't be called until _someTask.SetResult(..) is called. The calling-context is waiting around in memory somewhere.

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?

like image 262
BlueRaja - Danny Pflughoeft Avatar asked Jan 31 '15 22:01

BlueRaja - Danny Pflughoeft


People also ask

Do tasks need to be disposed?

“No. Don't bother disposing of your tasks, not unless performance or scalability testing reveals that you need to dispose of them based on your usage patterns in order to meet your performance goals.

Does Dispose get called automatically?

Dispose() will not be called automatically. If there is a finalizer it will be called automatically. Implementing IDisposable provides a way for users of your class to release resources early, instead of waiting for the garbage collector.

What is Dispose method in C# with example?

The dispose pattern is used for objects that implement the IDisposable interface, and is common when interacting with file and pipe handles, registry handles, wait handles, or pointers to blocks of unmanaged memory. This is because the garbage collector is unable to reclaim unmanaged objects.

What is TaskCompletionSource C#?

While Task. Run turns something synchronous into a Task (by running it on a separate thread), TaskCompletionSource turns something that is already asynchronous into a Task .


1 Answers

Updated, a good point by @SriramSakthivel, it turns out I've already answered a very similar question:

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

So I'm marking this one as a community 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?

If by the calling-context you mean the compiler-generated state machine object (which represents the state of the async method), then yes, it will indeed be finalized.

Example:

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

Output:

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"

Unlike this, with async methods there's no direct way of controlling the unwiding of using and finally.

like image 180
5 revs Avatar answered Sep 28 '22 09:09

5 revs