为什么GC收集我的对象时,我有一个参考吧? [英] Why does GC collects my object when I have a reference to it?

查看:196
本文介绍了为什么GC收集我的对象时,我有一个参考吧?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

让我们来看看下面的代码片段,显示的问题。

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

输出产生:

Start
Starting GC
~Synchronizer
GC Done

正如你可以看到同步获得Gc'd(更具体敲定,我们不知道的内存被回收或没有)。但为什么?为什么GC收集我的对象时,我有一个参考吧?

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?

研究: 我花了一些时间来调查幕后发生的事情,似乎由C#编译器生成的状态机保持为一个局部变量,而第一个等待命中后,似乎状态机本身超出范围。

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.

因此​​, GC.KeepAlive(同步); sync.Dispose(); 没有帮助的他们所生活的国家机器内部,其中作为国家机器本身是不存在的范围。

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#编译器不应该产生一个code这让我的同步实例超出范围时,我还是需要它。这是在C#编译器中的错误?还是我失去了一些东西根本?

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:我不是在寻找一个解决办法,而是为什么编译器做这样的解释吗?我用Google搜索,但并没有发现任何相关的问题,如果是重复后悔的。

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:我已经修改了 TaskCompletionSource 创作持有同步实例,仍然没有帮助。

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

推荐答案

什么 GC.KeepAlive(同步) - 这是的本身空白 - 在这里所做的仅仅是一个指令编译器中添加同步对象状态机结构启动生成。正如@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.

在另一方面,在 TaskCompletionSource tcs.Task 任务,内部使用内部启动,确实包含这样的引用(因为它拥有一个引用计谋延续回调,因此整个国家机器;回调注册与 tcs.Task 等待启动,创建 tcs.Task 和状态机)之间循环引用。然而,无论是 TCS tcs.Task 暴露的之外首页(其中也有可能是强引用),所以状态机的对象图是孤立的,并得到GC'ed。

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.

您可能已经避免了premature GC通过创建一个明确的强引用 TCS

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

或者,用一个更可读的版本异步

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

要借此研究远一​​点,考虑以下变化不大,注意 Task.Delay(Timeout.Infinite)和事实,我回来,用同步结果任务&LT;对象&gt; 。它没有得到任何好转:

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

国际海事组织,这是相当意外和不期望同步对象获得prematurely GC'ed之前,我可以通过访问任务。结果

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

现在,将 Task.Delay(Timeout.Infinite) Task.Delay(Int32.MaxValue)和它如预期工作的。

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

在内部,它涉及到对强引用计谋延续回调对象(委托本身),应​​同时导致该回调操作仍悬而未决举行(飞行)。我解释说这<一href="http://stackoverflow.com/questions/22268569/async-await-custom-awaiter-and-garbage-collector">Async/await,定制awaiter和垃圾收集。

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,事实证明这个操作可能会永无止境(如 Task.Delay(Timeout.Infinite)或不完整的 TaskCompletionSource )不应该影响这种行为。对于最自然的异步操作,例如很强的借鉴的确是由基础.NET code,这使得低级别的操作系统调用(如在例 Task.Delay(Int32.MaxValue),它传递回调到非托管的Win32 API计时器并持有到它与 GCHandle.Alloc )。

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).

在万一有任何级别的任何未决的非托管的呼叫(这可能是与 Task.Delay(Timeout.Infinite) TaskCompletionSource的情况下,冷工作,自定义awaiter),还有的地方没有明确的强引用,状态机的对象图是纯粹管理和孤立,所以意外GC确实会发生。

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.

我觉得这是一个小的设计折衷异步/计谋的基础设施,以避免的正常的内部<$ C多余的强引用$ C> ICriticalNotifyCompletion :: UnsafeOnCompleted 标准 TaskAwaiter

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.

总之,一个可能是通用的解决方案是很容易实现,使用自定义awaiter(姑且称之为 StrongAwaiter ):

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 本身(通用和非通用):

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


更新,这里是说明保持异步状态机活着的重要性,现实生活中的Win32互操作的例子。发布版本会崩溃,如果 GCHandle.Alloc(TCS) gch.Free()行注释掉。无论是回调 TCS 已被固定为它工作正常。另外,等待tcs.Task.WithStrongAwaiter()可以用来代替,利用上述 StrongAwaiter


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

这篇关于为什么GC收集我的对象时,我有一个参考吧?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆