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

让我们看一下显示问题的以下片段。

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 tcs; public Synchronizer() { tcs = new TaskCompletionSource(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) – 它本身是空白的 – 这里只是指令编译器将sync对象添加到为Start生成的状态机struct 。 正如@usr指出的那样, Start返回给调用者的外部任务包含对这个内部状态机的引用。

另一方面,在Start内部使用的TaskCompletionSourcetcs.Task任务确实包含这样的引用(因为它包含对await continuation回调的引用,因此包含对整个状态机的引用;回调是在tcs.Task注册时注册的在Start内部await ,在tcs.Task和状态机之间创建一个循环引用。 但是, tcstcs.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作为TaskResult的事实。 它没有变得更好:

  private static async Task Start() { Console.WriteLine("Start"); Synchronizer sync = new Synchronizer(); await Task.Delay(Timeout.Infinite); // OR: await new Task(() => 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,在我通过task.Result访问它之前, sync对象过早地task.Result已经是非常意外和不可取的

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

在内部,它归结为await continuation回调对象(委托本身)上的强引用,该对象应该在导致该回调的操作仍在等待(在飞行中)时保持。 我在“ 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基础设施中的一个小设计权衡,以避免在标准TaskAwaiter ICriticalNotifyCompletion::UnsafeOnCompleted中进行通常冗余的强引用。

无论如何,一个可能通用的解决方案很容易实现,使用自定义awaiter(让我们称之为StrongAwaiter ):

 private static async Task 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 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 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(); 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根目录访问sync 。 唯一的sync参考来自async状态机。 该状态机不会从任何地方引用。 有点令人惊讶的是它没有从Task或底层TaskCompletionSource

出于这个原因, sync ,状态机和TaskCompletionSource已经死了。

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

如果我写

 void F(Task t) { GC.KeepAlive(t); } 

然后这不会保持任何活力。 我实际上需要用某些东西调用F (或者必须可以调用它)。 只有KeepAlive存在什么都不做。

你认为你仍然引用了Synchronizer,因为你假设你的TaskCompletionSource仍然是对Synchronizer的引用,你的TaskCompletionSource仍然是“活着的”(由GC根引用)。 其中一个假设是不对的。

现在,忘掉你的TaskCompletionSource

更换线

 return tcs.Task; 

例如

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

那么你将不会再次进入析构函数。

结论是:如果要确保对象不会被垃圾收集,那么您必须明确地对它进行强有力的引用。 不要认为对象是“安全的”,因为它是由不在您控制范围内的东西引用的。