Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Garbage collection async methods

Tags:

c#

async-await

I just noticed something really strange with regards to garbage collection.

The WeakRef method collects the object as expected while the async method reports that the object is still alive even though we have forced a garbage collection. Any ideas why ?

class Program
{
    static void Main(string[] args)
    {
        WeakRef();
        WeakRefAsync().Wait();
    }

    private static void WeakRef()
    {
        var foo = new Foo();
        WeakReference fooRef = new WeakReference(foo);
        foo = null;
        GC.Collect();
        Debug.Assert(!fooRef.IsAlive);
    }

    private static async Task WeakRefAsync()
    {
        var foo = new Foo();
        WeakReference fooRef = new WeakReference(foo);
        foo = null;
        GC.Collect();
        Debug.Assert(!fooRef.IsAlive);
    }
}


public class Foo
{

}
like image 864
seesharper Avatar asked Feb 03 '17 11:02

seesharper


People also ask

Can dispose be async?

DisposeAsync() method when you need to perform resource cleanup, just as you would when implementing a Dispose method. One of the key differences, however, is that this implementation allows for asynchronous cleanup operations. The DisposeAsync() returns a ValueTask that represents the asynchronous disposal operation.

What does an async method do?

An async method runs synchronously until it reaches its first await expression, at which point the method is suspended until the awaited task is complete. In the meantime, control returns to the caller of the method, as the example in the next section shows.

What happens when you call async method?

The call to the async method starts an asynchronous task. However, because no Await operator is applied, the program continues without waiting for the task to complete. In most cases, that behavior isn't expected.

What is async return method?

Async functions always return a promise. If the return value of an async function is not explicitly a promise, it will be implicitly wrapped in a promise. Note: Even though the return value of an async function behaves as if it's wrapped in a Promise.resolve , they are not equivalent.


1 Answers

The WeakRef method collects the object as expected

There's no reason to expect that. Trying in Linqpad, it doesn't happen in a debug build, for example, though other valid compilations of both debug and release builds could have either behaviour.

Between the compiler and the jitter, they are free to optimise out the null-assignment (nothing uses foo after it, after all) in which case the GC could still see the thread as having a reference to the object and not collect it. Conversely, if there was no assignment of foo = null they'd be free to realise that foo isn't used any more and re-use the memory or register that had been holding it to hold fooRef (or indeed for something else entirely) and collect foo.

So, since both with and without the foo = null it's valid for the GC to see foo as either rooted or not rooted, we can reasonably expect either behaviour.

Still, the behaviour seen is a reasonable expectation as to what would probably happen, but that it's not guaranteed is worth pointing out.

Okay, that aside, let's look at what actually happens here.

The state-machine produced by the async method is a struct with fields corresponding to the locals in the source.

So the code:

var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
GC.Collect();

Is a bit like:

this.foo = new Foo();
this.fooRef = new WeakReference(foo);
this.foo = null;
GC.Collect();

But field accesses always have something going on locally. So in that regard it's almost like:

var temp0 = new Foo();
this.foo = temp0;
var temp1 = new WeakReference(foo);
this.fooRef = temp1;
var temp2 = null;
this.foo = temp2;
GC.Collect();

And temp0 hasn't been nulled, so the GC finds the Foo as rooted.

Two interesting variants of your code are:

var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
await Task.Delay(0);
GC.Collect();

And:

var foo = new Foo();
WeakReference fooRef = new WeakReference(foo);
foo = null;
await Task.Delay(1);
GC.Collect();

When I ran it (again, reasonable differences in how the memory/registers for locals is dealt with could result in different outcomes) the first has the same behaviour of your example, because while it calls into another Task method and awaits it, that method returns a completed task so the await immediately moves onto the next thing within the same underlying method call, which is the GC.Collect().

The second has the behaviour of seeing the Foo collected, because the await returns at that point and then the state-machine has its MoveNext() method called again roughly a millisecond later. Since it's a new call to the behind-the-scenes method, there's no local reference to the Foo so the GC can indeed collect it.

Incidentally, it's also possible that one day the compiler will not produce fields for those locals that don't live across await boundaries, which would be an optimisation that would still produce correct behaviour. If that was to happen then your two methods would become much more similar in underlying behaviour and hence more likely to be similar in observed behaviour.

like image 148
Jon Hanna Avatar answered Oct 18 '22 07:10

Jon Hanna