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

标签 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:我不是在寻找解决方法,而是在寻找编译器为何这样做的解释?我用谷歌搜索,但没有找到任何相关问题,如果重复,我们深表歉意。

Update1:我修改了 TaskCompletionSource 创建以保存 Synchronizer 实例,但仍然没有帮助。

最佳答案

什么 GC.KeepAlive(sync) - 这是 blank by itself - 这里只是指示编译器添加 sync反对状态机 structStart 生成.正如@usr 所指出的,Start 返回的外部 任务对其调用者的调用包含对此内部状态机的引用。

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

您可以通过创建对 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 的事实作为 Result对于 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();
    }

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

现在,更改 Task.Delay(Timeout.Infinite)Task.Delay(Int32.MaxValue)一切都按预期工作。

在内部,归结为对 await 的强引用继续回调对象(委托(delegate)本身),在导致该回调的操作仍未决(飞行中)时应持有。我在“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 、自定义等待者的情况),则没有明确的强引用,< strong>状态机的对象图是纯托管和隔离的,因此确实会发生意外的 GC。

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

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

关于c# - 为什么 GC 在我引用它时收集我的对象?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26957311/

相关文章:

c# - 如何在 C# 中进行只进、只读的 WMI 查询?

.net 内存未回收

C# 子类最佳实践

c# - 原始数组是否需要整数作为索引

c# - 如何在 C# 的 CheckedListBox 中对项目进行分组并向每个组添加标签

Java:比较 Netbeans 中的内存堆转储

python - python什么时候删除变量?

c# - 我可以在 UWP 应用程序中使用 SQL CE 数据库吗?

c# - 生产环境中的 HttpPostedFileBase null,但测试环境中不是

c# - WebBrowser HtmlElement.GetAttribute ("href") 前置主机名