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

时间:2014-11-16 13:13:33

标签: c# .net garbage-collection async-await

让我们看一下显示问题的以下片段。

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

输出产生:

Start
Starting GC
~Synchronizer
GC Done

正如您所看到的,sync获得Gc'd(更具体地说,最终确定,我们不知道内存是否被回收)。但为什么?为什么GC会在我引用它时收集我的对象?

研究: 我花了一些时间研究幕后发生的事情,似乎C#编译器生成的状态机被保存为局部变量,并且在第一次await命中之后,似乎状态机本身就出现了范围。

因此,GC.KeepAlive(sync);sync.Dispose();没有帮助,因为它们位于状态机内部,而状态机本身不在其中。

C#编译器不应该生成一个代码,当我仍然需要它时,我的sync实例会超出范围。这是C#编译器中的错误吗?或者我错过了一些基本的东西?

PS:我不是在寻找解决方法,而是解释为什么编译器会这样做?我用谷歌搜索,但没有发现任何相关问题,如果它是重复的抱歉。

更新1:我已修改TaskCompletionSource创建以保留Synchronizer实例,但仍无效。

3 个答案:

答案 0 :(得分:8)

无法从任何GC根目录访问

sync。对sync的唯一引用来自async状态机。该状态机不会从任何地方引用。有点令人惊讶it is not referenced from the Task or the underlying TaskCompletionSource.

因此sync,状态机和TaskCompletionSource已经死了。

添加GC.KeepAlive并不会阻止收集。如果对象引用实际上可以到达此语句,它只会阻止收集。

如果我写

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

然后这不会让任何保持活着。我实际上需要用某些东西调用F(或者必须可以调用它)。只有KeepAlive的存在不起作用。

答案 1 :(得分:7)

GC.KeepAlive(sync) - 这是blank by itself - 这里只是指示编译器将sync对象添加到为{{1}生成的状态机struct }}。正如@usr指出的那样,Start向其调用者返回的外部任务包含对此内部状态机的引用

另一方面,Start内部使用的TaskCompletionSource tcs.Task任务确实包含此类引用(因为它包含对{{1}的引用继续回调,从而整个状态机;回调在Start await内注册tcs.Task,在await和状态机之间创建循环引用。但是,Starttcs.Task都没有公开 tcs之外(它可能是强引用的),因此状态机的对象图被隔离并获得GC。

您可以通过创建对tcs.Task的明确强引用来避免过早的GC:

Start

或者,使用tcs的更易读的版本:

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

为了进一步深入研究,请考虑以下一点变化,请注意async以及我返回并使用public async Task SynchronizeAsync() { var gch = GCHandle.Alloc(tcs); try { await tcs.Task; } finally { gch.Free(); } } 作为Task.Delay(Timeout.Infinite) sync的事实。它没有变得更好:

Result

IMO,在我通过Task<object> 访问之前, 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(); } 对象过早地被GC过期,这是非常意外和不受欢迎的。

现在,将sync更改为task.Result,这一切都按预期工作。

在内部,它归结为Task.Delay(Timeout.Infinite)延续回调对象(委托本身)上的强引用,当导致该回调的操作仍然未决(在飞行中)时应该保持该引用。我在&#34; Async/await, custom awaiter and garbage collector&#34;。

中解释了这一点

IMO,这个操作可能永无止境的事实(如Task.Delay(Int32.MaxValue)或不完整的await)不应该影响这种行为。对于大多数自然异步操作,这种强引用确实由底层.NET代码所持有,后者生成低级OS调用(如Task.Delay(Timeout.Infinite)的情况,它将回调传递给非托管Win32计时器API并保持不变TaskCompletionSource)。

如果任何级别上没有待处理的非托管呼叫(可能是Task.Delay(Int32.MaxValue)GCHandle.Alloc,冷Task.Delay(Timeout.Infinite),自定义等待的情况),则没有明确的强大的引用,状态机的对象图是纯粹的管理和隔离,因此意外的GC确实发生了。

我认为这是TaskCompletionSource基础架构中的一个小设计权衡,以避免在标准Task的{​​{1}}内进行正常冗余强引用。< / p>

无论如何,一个可能通用的解决方案很容易实现,使用自定义awaiter(让我们称之为async/await):

ICriticalNotifyCompletion::UnsafeOnCompleted

TaskAwaiter本身(通用和非通用):

StrongAwaiter

<小时/> 更新,这是一个真实的Win32互操作示例,说明了保持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; } 状态机活着的重要性。如果注释掉StrongAwaiterpublic 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; } } } 行,则发布版本将崩溃。必须固定asyncGCHandle.Alloc(tcs)才能使其正常工作。或者,也可以使用gch.Free()代替callback

tcs

答案 2 :(得分:1)

您认为您仍然引用了Synchronizer,因为您假设您的TaskCompletionSource仍然是对Synchronizer的引用,并且您的TaskCompletionSource仍然是“活着的”(由GC根引用)。其中一个假设是不对的。

现在,忘掉你的TaskCompletionSource

替换

return tcs.Task;

例如

return Task.Run(() => { while (true) { } });

然后你不会再次进入析构函数。

结论是: 如果要确保不会对对象进行垃圾回收,则必须明确强制引用它。不要认为对象是“安全的”,因为它是由你无法控制的东西引用的。