C# 当我有一个对我的对象的引用时,为什么GC收集它?

C# 当我有一个对我的对象的引用时,为什么GC收集它?,c#,.net,garbage-collection,async-await,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();

让我们看一下下面显示问题的代码片段

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;
    }
}
正如您所看到的,
sync
getsgc'd(更具体地说,我们不知道内存是否被回收)。但是为什么呢?当我有对我的对象的引用时,GC为什么要收集它

研究: 我花了一些时间调查幕后发生的事情,似乎C#编译器生成的状态机作为局部变量保留,在第一次点击
wait
后,状态机本身似乎超出了范围

所以,
GC.KeepAlive(同步)
sync.Dispose()没有帮助,因为它们位于状态机内部,而状态机本身不在范围内

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

PS:我不是在寻找解决方法,而是解释编译器为什么会这样做?我在谷歌上搜索了一下,但没有发现任何相关问题,如果是重复的话,很抱歉


Update1:我已经修改了
TaskCompletionSource
创建以保存
同步器
实例,这仍然没有帮助。

sync
根本无法从任何GC根目录访问。对
sync
的唯一引用来自
async
状态机。该状态机未从任何位置引用。有点奇怪

由于这个原因,
sync
,状态机和
TaskCompletionSource
都死了

添加
GC.KeepAlive
本身不会阻止收集。仅当对象引用实际可以到达此语句时,它才阻止收集

如果我写

void F(Task t) { GC.KeepAlive(t); }
那么这并不能让任何东西存活下来。实际上,我需要用一些东西调用
F
(或者必须能够调用它)。仅仅存在一个
KeepAlive
并没有任何作用。

GC.KeepAlive(sync)
-这是-所做的只是一条指令,编译器可以将
sync
对象添加到为
Start
生成的状态机
struct
。正如@usr指出的,
Start
返回给调用方的外部任务不包含对该内部状态机的引用

另一方面,在
Start
内部使用的
TaskCompletionSource
tcs.Task
任务确实包含这样的引用(因为它持有对
await
连续回调的引用,从而包含整个状态机;回调在
await
内部
Start
时注册到
tcs.Task
,在
tcs.Task
和状态机之间创建循环引用)但是,无论是
tcs
还是
tcs.Task
都不会暴露在
Start
(它可能被强引用的地方)之外,因此状态机的对象图是隔离的,并得到GC'ed

您可以通过创建对
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();
    }
}

为了进一步研究,考虑下面的小变化,注意<代码>任务。Delay(超时,无限)以及我返回并使用<代码>同步< /代码>作为<代码>结果<代码> > <代码>任务<代码>。

    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();
    }
strong等待者
本身(通用和非通用):


您认为您仍然引用同步器,因为您假设您的TaskCompletionSource仍然引用同步器,并且您的TaskCompletionSource仍然“活动”(由GC根引用)。其中一个假设是不正确的

现在,忘掉你的
TaskCompletionSource

更换线路

return tcs.Task;
例如

return Task.Run(() => { while (true) { } });
这样你就不会再进入析构函数了

结论是:
如果要确保对象不会被垃圾收集,则必须显式地对其进行强引用。不要假定对象是“安全的”因为它被不在您控制范围内的某个对象引用。

您似乎没有从
private static async Task Start
@GSerg返回任何内容。为什么这很重要?因为您有
var Task=Start()
-您分配的是什么?这就是
async
的工作方式,它将创建一个
Task
对象。也许可以让
GC有所了解。但是,KeepAlive
不能内联,这是它唯一可以使用的点,也是它在框架中存在的点。Dispose需要一个对象引用,因为它是一个实例方法。
 GC.KeepAlive(sync)
无法内联,它被标记为
MethodImpl.noinline
我看到您添加了“因为您等待的任务从未完成”。运行时如何知道这一点以确保此任务永远不会完成,从而消除任务完成时将使用的任何代码和引用。这是一个暂停问题,运行时无法知道这一点。因此,尽管在这种情况下无法访问dispose,但我无法看到运行时如何知道certa的这一点in.KeepAlive仅在对象实际可访问时保持其活动状态。
if(false)KeepAlive(…)
不能让任何东西保持活力。@LasseV.Karlsen,因为值得停下来的问题并不意味着编译器永远无法决定任何程序。它说没有编译器可以决定每个程序。对于许多程序或片段,编译器很有可能找到它,而且有相当多的编译器会基于此进行优化。我看不到这里的实现质量问题。像往常一样,GC的工作对代码是看不见的。如果有任何方法可以观察要最终确定的对象的状态,它将不会最终确定。除非您播放
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;
        }
    }
}
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);
    }
}
return tcs.Task;
return Task.Run(() => { while (true) { } });