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

Sri*_*vel 13 .net c# 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

输出产生:

Start
Starting GC
~Synchronizer
GC Done
Run Code Online (Sandbox Code Playgroud)

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

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

所以,GC.KeepAlive(sync);sync.Dispose();没有帮助,因为他们住在状态机内部,因为状态机本身不在范围内.

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

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

Update1:我已经修改了TaskCompletionSource创建来保存Synchronizer实例,但仍然没有帮助.

usr*_*usr 8

sync根本无法从任何GC根目录访问.唯一的参考sync是来自async状态机.该状态机不会从任何地方引用.有点令人惊讶的是,它没有引用Task或潜在的TaskCompletionSource.

出于这个原因sync,国家机器和TaskCompletionSource死机.

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

如果我写

void F(Task t) { GC.KeepAlive(t); }
Run Code Online (Sandbox Code Playgroud)

然后,这不会保持任何活力.我实际上需要调用F某些东西(或者必须可以调用它).仅仅存在一KeepAlive无所作为.

  • @ LasseV.Karlsen,因为值得停止的问题并没有说编译器永远不能决定*any*程序.它说没有编译器可以决定每个*程序.对于许多程序或片段,编译器很有可能弄明白,而且很多编译器都会根据它进行优化. (3认同)
  • 但是,GC.KeepAlive`无法内联,这是它唯一被使用的点,也是它在框架中存在的唯一要点. (2认同)
  • 我看到你添加了"因为你正在等待的任务永远不会完成".如何确保此任务永远不会完成,运行时如何知道这一点,从而消除任何代码,从而消除任务完成时使用的引用.这是暂停问题,运行时无法知道这一点.因此,虽然在这种情况下不会达到处理,但我无法看到运行时如何知道这一点. (2认同)

nos*_*tio 7

什么GC.KeepAlive(sync)- 它本身空白的 - 这里只是指令编译器将sync对象添加到为其struct生成的状态机Start.正如@usr指出的那样,返回给它的调用者的外部任务包含对这个内部状态机的引用.Start

在另一方面,所述TaskCompletionSourcetcs.Task任务,用于在内部内Start,确实包含这样的引用(因为它保持对所述的基准await延续回调从而对整个状态机;回调登记tcs.Taskawait内部Start,创建之间循环引用tcs.Task和国家机器).然而,无论是tcs也不tcs.Task露出 Start(其中,它可能是强引用),所以状态机的对象图是分离的,并得到GC'ed.

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

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

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

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

为了进一步研究这个研究,请考虑以下一点变化,注意Task.Delay(Timeout.Infinite)以及我返回并sync用作Resultfor 的事实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();
    }
Run Code Online (Sandbox Code Playgroud)

IMO,sync在我通过它访问它之前,对象过早地被GC过度是非常意外和不可取的task.Result.

现在,Task.Delay(Timeout.Infinite)改为Task.Delay(Int32.MaxValue),它都按预期工作.

在内部,它归结为对await继续回调对象(委托本身)的强引用,当执行该回调的操作仍处于待决状态(在飞行中)时应该保持该引用.我在" Async/await,custom awaiter and garbage collector "中解释了这一点.

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

如果有任何级别没有挂起的非托管的呼叫(这可能是与案件Task.Delay(Timeout.Infinite),TaskCompletionSource是一种冷Task,定制awaiter),有一个地方没有明确的强引用,状态机的对象图纯粹是管理和孤立的,所以意外的GC确实发生了.

我认为这是async/await基础设施中的一个小设计权衡,以避免在标准内部进行通常冗余的强引用.ICriticalNotifyCompletion::UnsafeOnCompletedTaskAwaiter

无论如何,一个可能通用的解决方案很容易实现,使用自定义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;
}
Run Code Online (Sandbox Code Playgroud)

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


更新,这是一个真实的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);
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 是的,这确实不明显.我从来没有注意到这个错误. (2认同)