简体   繁体   English

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

[英]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). 正如你可以看到sync获得Gc'd(更具体地说,最终确定,我们不知道内存是否被回收)。 But why? 但为什么? Why would GC collect my object when I have a reference to it? 为什么GC会在我引用它时收集我的对象?

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

So, GC.KeepAlive(sync); 所以, GC.KeepAlive(sync); and sync.Dispose(); 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. C#编译器不应该生成一个代码,当我仍然需要时,它会使我的sync实例超出范围。 Is this a bug in C# compiler? 这是C#编译器中的错误吗? Or am I missing something fundamental? 或者我错过了一些基本的东西?

PS: I'm not looking for a workaround, but rather a explanation of why the compiler does this? PS:我不是在寻找解决方法,而是解释为什么编译器会这样做? I googled, but didn't found any related questions, if it is duplicate sorry for that. 我用Google搜索,但没有找到任何相关的问题,如果它是重复的抱歉。

Update1: I've modified the TaskCompletionSource creation to hold the Synchronizer instance, that still doesn't help. Update1:我已经修改了TaskCompletionSource创建以保存Synchronizer实例,但仍无法提供帮助。

sync is simply not reachable from any GC root. 无法从任何GC根目录访问sync The only reference to sync is from the async state machine. 唯一的sync参考来自async状态机。 That state machine is not referenced from anywhere. 该状态机不会从任何地方引用。 Somewhat surprisingly it is not referenced from the Task or the underlying TaskCompletionSource . 有点令人惊讶的是它没有从Task或底层的TaskCompletionSource

For that reason sync , the state machine and the TaskCompletionSource are dead. 出于这个原因, sync ,状态机和TaskCompletionSource已经死了。

Adding a GC.KeepAlive does not prevent collection by itself. 添加GC.KeepAlive不会阻止自身收集。 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). 我实际上需要用某些东西调用F (或者必须可以调用它)。 The mere presence of a KeepAlive does nothing. 只有KeepAlive存在什么都不做。

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 . 什么GC.KeepAlive(sync) - 它本身空白的 - 这里只是指令编译器将sync对象添加到为Start生成的状态机struct As @usr pointed out, the outer task returned by Start to its caller does not contain a reference to this inner state machine. 正如@usr指出的那样, Start返回给调用者的外部任务包含对这个内部状态机的引用。

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). 另一方面,在Start内部使用的TaskCompletionSourcetcs.Task任务确实包含这样的引用(因为它包含对await continuation回调的引用,因此包含对整个状态机的引用;回调是在tcs.Task注册时注册的在Start内部await ,在tcs.Task和状态机之间创建一个循环引用。 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. 但是, tcstcs.Task都不会暴露 Start 之外 (它可能是强引用的),因此状态机的对象图被隔离并获得GC。

You could have avoided the premature GC by creating an explicit strong reference to tcs : 您可以通过创建对tcs的显式强引用来避免过早的GC:

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 : 或者,使用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> . 为了进一步研究这个问题,请考虑以下一点变化,注意Task.Delay(Timeout.Infinite)以及我返回并使用sync作为Task<object>Result的事实。 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 . IMO,在我通过task.Result访问它之前, sync对象过早地被task.Result是非常意外和不可取的

Now, change Task.Delay(Timeout.Infinite) to Task.Delay(Int32.MaxValue) and it all works as expected. 现在,将Task.Delay(Timeout.Infinite)更改为Task.Delay(Int32.MaxValue) ,它们都按预期工作。

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). 在内部,它归结为await continuation回调对象(委托本身)上的强引用,该对象应该在导致该回调的操作仍在等待(在飞行中)时保持。 I explained this in " Async/await, custom awaiter and garbage collector ". 我在“ 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. IMO,这个操作可能永无止境的事实(如Task.Delay(Timeout.Infinite)或不完整的TaskCompletionSource )不应该影响这种行为。 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 ). 对于大多数自然异步操作,这种强引用确实由底层.NET代码保存,后者生成低级OS调用(如Task.Delay(Int32.MaxValue) ,它将回调传递给非托管Win32计时器API)并坚持使用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. 如果在任何级别上没有挂起的非托管调用(可能是Task.Delay(Timeout.Infinite)TaskCompletionSource ,冷Task ,自定义等待者)的情况,则没有明确的强引用, 状态机的对象图是纯粹管理和隔离的 ,因此意外的GC确实发生了。

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 . 我认为这是async/await基础设施中的一个小设计权衡,以避免在标准TaskAwaiter ICriticalNotifyCompletion::UnsafeOnCompleted中进行通常冗余的强引用。

Anyhow, a possibly universal solution is quite easy to implement, using a custom awaiter (let's call it StrongAwaiter ): 总之,一个可能通用的解决方案很容易实现,使用自定义awaiter(让我们称之为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): StrongAwaiter本身(通用和非通用):

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. 更新 ,这是一个真实的Win32互操作示例,说明了保持async状态机活着的重要性。 The release build will crash if GCHandle.Alloc(tcs) and gch.Free() lines are commented out. 如果GCHandle.Alloc(tcs)gch.Free()行,则发布版本将崩溃。 Either callback or tcs has to be pinned for it to work properly. 必须固定callbacktcs才能使其正常工作。 Alternatively, await tcs.Task.WithStrongAwaiter() can be used instead, utilizing the above StrongAwaiter . 或者,可以使用await tcs.Task.WithStrongAwaiter() ,使用上面的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);
    }
}

You think that you are still reference to Synchronizer, because you assume that your TaskCompletionSource is still reference to the Synchronizer and your TaskCompletionSource is still "alive" (referenced by GC roots). 你认为你仍然引用了Synchronizer,因为你假设你的TaskCompletionSource仍然是对Synchronizer的引用,你的TaskCompletionSource仍然是“活着的”(由GC根引用)。 One of these assumption is not right. 其中一个假设是不对的。

Now, forget about your TaskCompletionSource 现在,忘掉你的TaskCompletionSource

replace the line 替换线

return tcs.Task;

by for example 例如

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

then you won't enter the Destructor ever again. 那么你将不会再次进入析构函数。

The conclusion is that: If you want to make sure that a object won't be garbage collected, then you will have to explicitly make a strong reference to it. 结论是:如果你想确保一个对象不会被垃圾收集,那么你必须明确地强烈引用它。 Don't assume that a object is "safe" because it is referenced by something not in your control. 不要认为对象是“安全的”,因为它是由不在您控制范围内的东西引用的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 垃圾收集器在收集 object 时是否设置了对 null 的引用? - does garbage collector set the reference to null when it collects an object? c#如果捕获新方法的异常,GC会收集此对象吗? - c# if catch exception of a new method, will GC collects this object? 为什么GC不收集未使用的对象 - Why GC does not collect an unused object 为什么在我处理 ArgumentException 时显示“可能的 null 参考”警告? - Why does it show "possible null reference" warning when I have handled the ArgumentException? 当我尝试调用我的方法时,为什么会出现对象引用错误? - Why do I get an object reference error when i try to call my method? 为什么当我有一个类时我的程序运行,而当我使用两个类时却不能运行? - Why does my program run when I have one class, but not when I use two? 为什么这个通用约束在似乎有循环引用时编译 - Why does this generic constraint compile when it seems to have a circular reference 为什么我的List &lt;&gt;对象上出现对象引用错误? - Why am I getting an object reference error on my List<> object? 为什么我必须在视图中引用我的模型? - Why do I have to reference my models in a view? 为什么我必须指定Uri作为我的服务参考? - Why do I have to specify a Uri for my service reference?
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM