为什么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
内部使用的TaskCompletionSource
的tcs.Task
任务确实包含这样的引用(因为它包含对await
continuation回调的引用,因此包含对整个状态机的引用;回调是在tcs.Task
注册时注册的在Start
内部await
,在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
作为Task
的Result
的事实。 它没有变得更好:
private static async Task
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()
行,则发布版本将崩溃。 必须固定callback
或tcs
才能使其正常工作。 或者,可以使用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) { } });
那么你将不会再次进入析构函数。
结论是:如果要确保对象不会被垃圾收集,那么您必须明确地对它进行强有力的引用。 不要认为对象是“安全的”,因为它是由不在您控制范围内的东西引用的。