【问题标题】:Why does GC collects my object when I have a reference to it?为什么当我引用我的对象时,GC 会收集它?
【发布时间】:2014-11-16 13:13:33
【问题描述】:

让我们看看下面的sn-p,它显示了问题。

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:我不是在寻找解决方法,而是在寻找编译器为什么这样做的解释?我google了一下,没有发现任何相关问题,如果重复的话,抱歉。

更新 1:我已修改 TaskCompletionSource 创建以保存 Synchronizer 实例,但这仍然没有帮助。

【问题讨论】:

  • 你似乎没有从private static async Task Start返回任何东西。
  • @GSerg 为什么这很重要?
  • 因为你有var task = Start(); - 你在分配什么?
  • async 就是这样工作的,它将创建一个Task 对象。
  • 也许this 能有所启发

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


【解决方案1】:

sync 根本无法从任何 GC 根访问。对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 的存在无济于事。

【讨论】:

  • GC.KeepAlive 不能内联,但是,这是它被使用的唯一点,也是它在框架中存在的唯一点。
  • Dispose 需要一个对象引用,因为它是一个实例方法。 GC.KeepAlive(sync)不能内联,用MethodImpl.NoInlining标记
  • 我看到您添加了“因为您等待的任务从未完成”。运行时如何确定此任务将永远不会完成,从而消除任何代码以及在任务完成时将使用的引用。这是停止问题,运行时无法知道这一点。因此,尽管在这种情况下不会达到 dispose,但我看不到运行时如何确定地知道这一点。
  • KeepAlive 仅在对象实际可访问时才使对象保持活动状态。 if(false) KeepAlive(...) 不会让任何东西保持活力。
  • @LasseV.Karlsen,因为停止问题的价值并不是说编译器永远无法决定 any 程序。它说没有编译器可以决定 every 程序。对于许多程序或片段,编译器很可能会弄清楚,并且相当多的编译器确实会基于此进行优化。
【解决方案2】:

GC.KeepAlive(sync)(即blank by itself)在这里所做的只是向编译器发出的一条指令,将sync 对象添加到为Start 生成的状态机struct。正如@usr 所指出的,Start 返回给它的调用者的outer 任务确实 包含对这个inner 状态机的引用。

另一方面,TaskCompletionSourcetcs.Task 任务在Start 内部使用,确实包含这样的引用(因为它包含对await 延续回调的引用,因此是整个状态机;回调在awaitStart 上注册tcs.Task,在tcs.Task 和状态机之间创建循环引用)。然而,tcstcs.Task 都没有暴露在外部 Start(它可能是强引用的),因此状态机的对象图是隔离的并被 GC'ed。 p>

您可以通过创建对 tcs 的显式强引用来避免过早的 GC:

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 作为ResultTask&lt;object&gt; 的事实。没有好转:

    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,sync 对象在我可以通过 task.Result 访问它之前被过早地 GC 处理,这是非常出乎意料和不受欢迎的。

现在,将Task.Delay(Timeout.Infinite) 更改为Task.Delay(Int32.MaxValue),一切正常。

在内部,它归结为对await 延续回调对象(委托本身)的强引用,当导致该回调的操作仍处于挂起状态(正在运行)时,它应该被持有。我在“Async/await, custom awaiter and garbage collector”中解释了这一点。

IMO,此操作可能永无止境(如Task.Delay(Timeout.Infinite) 或不完整的TaskCompletionSource)这一事实不应影响此行为。对于大多数自然异步操作,这种强引用确实由进行低级操作系统调用的底层 .NET 代码持有(例如 Task.Delay(Int32.MaxValue),它将回调传递给非托管 Win32 计时器 API 并保留它GCHandle.Alloc)。

如果在任何级别都没有挂起的非托管调用(Task.Delay(Timeout.Infinite)TaskCompletionSource、冷Task、自定义等待程序可能是这种情况),则没有明确的强引用,状态机的对象图是完全托管和隔离的,所以意外的 GC 确实会发生。

我认为这是async/await 基础架构中的一个小设计折衷,以避免在标准TaskAwaiterICriticalNotifyCompletion::UnsafeOnCompleted通常产生冗余强引用。

无论如何,一个可能通用的解决方案很容易实现,使用自定义等待器(我们称之为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 本身(通用和非通用):

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 互操作示例,说明保持async 状态机活动的重要性。如果注释掉 GCHandle.Alloc(tcs)gch.Free() 行,则发布版本将崩溃。必须固定callbacktcs 才能正常工作。或者,可以使用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);
    }
}

【讨论】:

  • 我在这里没有看到实施质量问题。与往常一样,GC 的工作对代码是不可见的。如果有任何方法可以观察到要完成的对象的状态,它不会被完成。除了,如果你纯粹为了观察终结行为而对终结者玩鬼鬼祟祟的把戏。此对象图星座不会影响任何合理的终结器。这几乎就像使用调试器检测 JIT 优化,然后抱怨 JIT 以可检测的方式修改了直线代码。 GC 和 JIT 并非在所有合理的情况下都可检测到。 (+1)
  • @usr,我尊重 CLR/C# 团队的这一设计决定,尽管最初我几乎认为这是一个错误。然而,我关心的是实际的互操作编码。例如,我遇到的某件事的剥离版本:dotnetfiddle.net/ESRnZk。如果 GCHandle.Alloc(tcs)gch.Free() 行被注释掉,它将崩溃,这在 IMO 中并不明显。必须固定 callbacktcs 才能正常工作。
  • 是的,这确实不明显。我永远不会注意到这个错误。
  • 感谢您的全面回答,您的链接回答非常有帮助。尽管我们很难手动保持实例处于活动状态,但我们必须接受这个事实:(再次感谢。
  • 在您的现实生活示例中,callback 已被 GC 处理,并且计时器尝试调用它,这就是异常所在吗?我在运行它时得到了 NRE,但是没有堆栈跟踪有多奇怪?
【解决方案3】:

您认为您仍然引用 Synchronizer,因为您假设您的 TaskCompletionSource 仍然引用 Synchronizer,并且您的 TaskCompletionSource 仍然“活动”(由 GC 根引用)。这些假设之一是不正确的。

现在,忘记你的TaskCompletionSource

换行

return tcs.Task;

例如

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

那么你就不会再进入析构函数了。

结论是: 如果你想确保一个对象不会被垃圾回收,那么你必须明确地对它进行强引用。不要假设一个对象是“安全的”,因为它被不受你控制的东西引用。

【讨论】:

    猜你喜欢
    • 2012-11-10
    • 2012-01-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-11-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多