Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Let's look at the following snippet which shows the problem.

class Program
{
    static void Main(string[] args)
    {
        var task = Start();
        Task.Run(() =>
        {
            Thread.Sleep(500);
            Console.WriteLine("Starting GC");
            GC.Collect();
            GC.WaitForPendingFinalizers();
            Console.WriteLine("GC Done");
        });

        task.Wait();

        Console.Read();
    }

    private static async Task Start()
    {
        Console.WriteLine("Start");
        Synchronizer sync = new Synchronizer();
        var task = sync.SynchronizeAsync();
        await task;

        GC.KeepAlive(sync);//Keep alive or any method call doesn't help
        sync.Dispose();//I need it here, But GC eats it :(
    }
}

public class Synchronizer : IDisposable
{
    private TaskCompletionSource<object> tcs;

    public Synchronizer()
    {
        tcs = new TaskCompletionSource<object>(this);
    }

    ~Synchronizer()
    {
        Console.WriteLine("~Synchronizer");
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose");
    }

    public Task SynchronizeAsync()
    {
        return tcs.Task;
    }
}

Output produces:

Start
Starting GC
~Synchronizer
GC Done

As you can see sync gets Gc'd(more specifically finalized, we don't know about memory gets reclaimed or not). But why? Why would GC collect my object when I have a reference to it?

Research: I've spent some time investigating what happens behind the scenes, It seems that state machine generated by the C# compiler is kept as a local variable, and after the first await hit, it seems that the state machine itself goes out of scope.

So, GC.KeepAlive(sync); and sync.Dispose(); doesn't help as they live inside the state machine where as state machine itself is not there in scope.

C# compiler shouldn't have generated a code which leaves my sync instance to go out of scope when I still need it. Is this a bug in C# compiler? Or am I missing something fundamental?

PS: I'm not looking for a workaround, but rather a explanation of why the compiler does this? I googled, but didn't found any related questions, if it is duplicate sorry for that.

Update1: I've modified the TaskCompletionSource creation to hold the Synchronizer instance, that still doesn't help.

like image 444
Sriram Sakthivel Avatar asked Nov 16 '14 13:11

Sriram Sakthivel


2 Answers

sync is simply not reachable from any GC root. The only reference to sync is from the async state machine. That state machine is not referenced from anywhere. Somewhat surprisingly it is not referenced from the Task or the underlying TaskCompletionSource.

For that reason sync, the state machine and the TaskCompletionSource are dead.

Adding a GC.KeepAlive does not prevent collection by itself. It only prevents collection if an object reference can actually reach this statement.

If I write

void F(Task t) { GC.KeepAlive(t); }

Then this does not keep anything alive. I actually need to call F with something (or it must be possible for it to be called). The mere presence of a KeepAlive does nothing.

like image 132
usr Avatar answered Sep 17 '22 18:09

usr


What GC.KeepAlive(sync) - which is blank by itself - does here is just an instruction to the compiler to add the sync object to the state machine struct generated for Start. As @usr pointed out, the outer task returned by Start to its caller does not contain a reference to this inner state machine.

On the other hand, the TaskCompletionSource's tcs.Task task, used internally inside Start, does contain such reference (because it holds a reference to the await continuation callback and thus the whole state machine; the callback is registered with tcs.Task upon await inside Start, creating a circular reference between tcs.Task and the state machine). However, neither tcs nor tcs.Task is exposed outside Start (where it could have been strong-referenced), so the state machine's object graph is isolated and gets GC'ed.

You could have avoided the premature GC by creating an explicit strong reference to tcs:

public Task SynchronizeAsync()
{
    var gch = GCHandle.Alloc(tcs);
    return tcs.Task.ContinueWith(
        t => { gch.Free(); return t; },
        TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}

Or, a more readable version using async:

public async Task SynchronizeAsync()
{
    var gch = GCHandle.Alloc(tcs);
    try
    {
        await tcs.Task;
    }
    finally
    {
        gch.Free();
    }
}

To take this research a bit further, consider the following little change, note Task.Delay(Timeout.Infinite) and the fact that I return and use sync as the Result for Task<object>. It doesn't get any better:

    private static async Task<object> Start()
    {
        Console.WriteLine("Start");
        Synchronizer sync = new Synchronizer();

        await Task.Delay(Timeout.Infinite); 

        // OR: await new Task<object>(() => sync);

        // OR: await sync.SynchronizeAsync();

        return sync;
    }

    static void Main(string[] args)
    {
        var task = Start();
        Task.Run(() =>
        {
            Thread.Sleep(500);
            Console.WriteLine("Starting GC");
            GC.Collect();
            GC.WaitForPendingFinalizers();
            Console.WriteLine("GC Done");
        });

        Console.WriteLine(task.Result);

        Console.Read();
    }

IMO, it's quite unexpected and undesirable that sync object gets prematurely GC'ed before I could access it via task.Result.

Now, change Task.Delay(Timeout.Infinite) to Task.Delay(Int32.MaxValue) and it all works as expected.

Internally, it comes down to the strong reference on the await continuation callback object (the delegate itself) which should be held while the operation resulting in that callback is still pending (in flight). I explained this in "Async/await, custom awaiter and garbage collector".

IMO, the fact that this operation might be never-ending (like Task.Delay(Timeout.Infinite) or incomplete TaskCompletionSource) should not be affecting this behavior. For most of naturally asynchronous operations, such strong reference is indeed held by the underlying .NET code which makes low-level OS calls (like in case with Task.Delay(Int32.MaxValue), which passes the callback to the unmanaged Win32 timer API and holds on to it with GCHandle.Alloc).

In case there is no pending unmanaged calls on any level (which might be the case with Task.Delay(Timeout.Infinite), TaskCompletionSource, a cold Task, a custom awaiter), there is no explicit strong references in place, the state machine's object graph is purely managed and isolated, so the unexpected GC does happen.

I think this is a small design trade-off in async/await infrastructure, to avoid making normally redundant strong references inside ICriticalNotifyCompletion::UnsafeOnCompleted of standard TaskAwaiter.

Anyhow, a possibly universal solution is quite easy to implement, using a custom awaiter (let's call it StrongAwaiter):

private static async Task<object> Start()
{
    Console.WriteLine("Start");
    Synchronizer sync = new Synchronizer();

    await Task.Delay(Timeout.Infinite).WithStrongAwaiter();

    // OR: await sync.SynchronizeAsync().WithStrongAwaiter();

    return sync;
}

StrongAwaiter itself (generic and non-generic):

public static class TaskExt
{
    // Generic Task<TResult>

    public static StrongAwaiter<TResult> WithStrongAwaiter<TResult>(this Task<TResult> @task)
    {
        return new StrongAwaiter<TResult>(@task);
    }

    public class StrongAwaiter<TResult> :
        System.Runtime.CompilerServices.ICriticalNotifyCompletion
    {
        Task<TResult> _task;
        System.Runtime.CompilerServices.TaskAwaiter<TResult> _awaiter;
        System.Runtime.InteropServices.GCHandle _gcHandle;

        public StrongAwaiter(Task<TResult> task)
        {
            _task = task;
            _awaiter = _task.GetAwaiter();
        }

        // custom Awaiter methods
        public StrongAwaiter<TResult> GetAwaiter()
        {
            return this;
        }

        public bool IsCompleted
        {
            get { return _task.IsCompleted; }
        }

        public TResult GetResult()
        {
            return _awaiter.GetResult();
        }

        // INotifyCompletion
        public void OnCompleted(Action continuation)
        {
            _awaiter.OnCompleted(WrapContinuation(continuation));
        }

        // ICriticalNotifyCompletion
        public void UnsafeOnCompleted(Action continuation)
        {
            _awaiter.UnsafeOnCompleted(WrapContinuation(continuation));
        }

        Action WrapContinuation(Action continuation)
        {
            Action wrapper = () =>
            {
                _gcHandle.Free();
                continuation();
            };

            _gcHandle = System.Runtime.InteropServices.GCHandle.Alloc(wrapper);
            return wrapper;
        }
    }

    // Non-generic Task

    public static StrongAwaiter WithStrongAwaiter(this Task @task)
    {
        return new StrongAwaiter(@task);
    }

    public class StrongAwaiter :
        System.Runtime.CompilerServices.ICriticalNotifyCompletion
    {
        Task _task;
        System.Runtime.CompilerServices.TaskAwaiter _awaiter;
        System.Runtime.InteropServices.GCHandle _gcHandle;

        public StrongAwaiter(Task task)
        {
            _task = task;
            _awaiter = _task.GetAwaiter();
        }

        // custom Awaiter methods
        public StrongAwaiter GetAwaiter()
        {
            return this;
        }

        public bool IsCompleted
        {
            get { return _task.IsCompleted; }
        }

        public void GetResult()
        {
            _awaiter.GetResult();
        }

        // INotifyCompletion
        public void OnCompleted(Action continuation)
        {
            _awaiter.OnCompleted(WrapContinuation(continuation));
        }

        // ICriticalNotifyCompletion
        public void UnsafeOnCompleted(Action continuation)
        {
            _awaiter.UnsafeOnCompleted(WrapContinuation(continuation));
        }

        Action WrapContinuation(Action continuation)
        {
            Action wrapper = () =>
            {
                _gcHandle.Free();
                continuation();
            };

            _gcHandle = System.Runtime.InteropServices.GCHandle.Alloc(wrapper);
            return wrapper;
        }
    }
}


Updated, here is a real-life Win32 interop example illustrating the importance of keeping the async state machine alive. The release build will crash if GCHandle.Alloc(tcs) and gch.Free() lines are commented out. Either callback or tcs has to be pinned for it to work properly. Alternatively, await tcs.Task.WithStrongAwaiter() can be used instead, utilizing the above StrongAwaiter.
using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    public class Program
    {
        static async Task TestAsync()
        {
            var tcs = new TaskCompletionSource<bool>();

            WaitOrTimerCallbackProc callback = (a, b) =>
                tcs.TrySetResult(true);

            //var gch = GCHandle.Alloc(tcs);
            try
            {
                IntPtr timerHandle;
                if (!CreateTimerQueueTimer(out timerHandle,
                        IntPtr.Zero,
                        callback,
                        IntPtr.Zero, 2000, 0, 0))
                    throw new System.ComponentModel.Win32Exception(
                        Marshal.GetLastWin32Error());

                await tcs.Task;
            }
            finally
            {
                //gch.Free();

                GC.KeepAlive(callback);
            }
        }

        public static void Main(string[] args)
        {
            var task = TestAsync();

            Task.Run(() =>
            {
                Thread.Sleep(500);
                Console.WriteLine("Starting GC");
                GC.Collect();
                GC.WaitForPendingFinalizers();
                Console.WriteLine("GC Done");
            });

            task.Wait();

            Console.WriteLine("completed!");
            Console.Read();
        }

        // p/invoke
        delegate void WaitOrTimerCallbackProc(IntPtr lpParameter, bool TimerOrWaitFired);

        [DllImport("kernel32.dll")]
        static extern bool CreateTimerQueueTimer(out IntPtr phNewTimer,
           IntPtr TimerQueue, WaitOrTimerCallbackProc Callback, IntPtr Parameter,
           uint DueTime, uint Period, uint Flags);
    }
}
like image 40
noseratio Avatar answered Sep 18 '22 18:09

noseratio