[英]Why does GC collects my object when I have a reference to it?
Let's look at the following snippet which shows the problem. 让我们看一下显示问题的以下片段。
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;
}
}
Output produces: 输出产生:
Start
Starting GC
~Synchronizer
GC Done
As you can see sync
gets Gc'd(more specifically finalized, we don't know about memory gets reclaimed or not). 正如你可以看到
sync
获得Gc'd(更具体地说,最终确定,我们不知道内存是否被回收)。 But why? 但为什么? Why would GC collect my object when I have a reference to it?
为什么GC会在我引用它时收集我的对象?
Research: I've spent some time investigating what happens behind the scenes, It seems that state machine generated by the C# compiler is kept as a local variable, and after the first await
hit, it seems that the state machine itself goes out of scope. 研究:我花了一些时间研究幕后发生的事情,似乎C#编译器生成的状态机被保存为局部变量,并且在第一次
await
命中之后,似乎状态机本身超出了范围。
So, GC.KeepAlive(sync);
所以,
GC.KeepAlive(sync);
and sync.Dispose();
和
sync.Dispose();
doesn't help as they live inside the state machine where as state machine itself is not there in scope. 没有帮助,因为他们住在状态机内部,因为状态机本身不在范围内。
C# compiler shouldn't have generated a code which leaves my sync
instance to go out of scope when I still need it. C#编译器不应该生成一个代码,当我仍然需要时,它会使我的
sync
实例超出范围。 Is this a bug in C# compiler? 这是C#编译器中的错误吗? Or am I missing something fundamental?
或者我错过了一些基本的东西?
PS: I'm not looking for a workaround, but rather a explanation of why the compiler does this? PS:我不是在寻找解决方法,而是解释为什么编译器会这样做? I googled, but didn't found any related questions, if it is duplicate sorry for that.
我用Google搜索,但没有找到任何相关的问题,如果它是重复的抱歉。
Update1: I've modified the TaskCompletionSource
creation to hold the Synchronizer
instance, that still doesn't help. Update1:我已经修改了
TaskCompletionSource
创建以保存Synchronizer
实例,但仍无法提供帮助。
sync
is simply not reachable from any GC root. 无法从任何GC根目录访问
sync
。 The only reference to sync
is from the async
state machine. 唯一的
sync
参考来自async
状态机。 That state machine is not referenced from anywhere. 该状态机不会从任何地方引用。 Somewhat surprisingly it is not referenced from the
Task
or the underlying TaskCompletionSource
. 有点令人惊讶的是它没有从
Task
或底层的TaskCompletionSource
。
For that reason sync
, the state machine and the TaskCompletionSource
are dead. 出于这个原因,
sync
,状态机和TaskCompletionSource
已经死了。
Adding a GC.KeepAlive
does not prevent collection by itself. 添加
GC.KeepAlive
不会阻止自身收集。 It only prevents collection if an object reference can actually reach this statement. 如果对象引用实际上可以到达此语句,它只会阻止收集。
If I write 如果我写
void F(Task t) { GC.KeepAlive(t); }
Then this does not keep anything alive. 然后,这不会保持任何活力。 I actually need to call
F
with something (or it must be possible for it to be called). 我实际上需要用某些东西调用
F
(或者必须可以调用它)。 The mere presence of a KeepAlive
does nothing. 只有
KeepAlive
存在什么都不做。
What GC.KeepAlive(sync)
- which is blank by itself - does here is just an instruction to the compiler to add the sync
object to the state machine struct
generated for Start
. 什么
GC.KeepAlive(sync)
- 它本身是空白的 - 这里只是指令编译器将sync
对象添加到为Start
生成的状态机struct
。 As @usr pointed out, the outer task returned by Start
to its caller does not contain a reference to this inner state machine. 正如@usr指出的那样,
Start
返回给调用者的外部任务不包含对这个内部状态机的引用。
On the other hand, the TaskCompletionSource
's tcs.Task
task, used internally inside Start
, does contain such reference (because it holds a reference to the await
continuation callback and thus the whole state machine; the callback is registered with tcs.Task
upon await
inside Start
, creating a circular reference between tcs.Task
and the state machine). 另一方面,在
Start
内部使用的TaskCompletionSource
的tcs.Task
任务确实包含这样的引用(因为它包含对await
continuation回调的引用,因此包含对整个状态机的引用;回调是在tcs.Task
注册时注册的在Start
内部await
,在tcs.Task
和状态机之间创建一个循环引用。 However, neither tcs
nor tcs.Task
is exposed outside Start
(where it could have been strong-referenced), so the state machine's object graph is isolated and gets GC'ed. 但是,
tcs
和tcs.Task
都不会暴露在 Start
之外 (它可能是强引用的),因此状态机的对象图被隔离并获得GC。
You could have avoided the premature GC by creating an explicit strong reference to tcs
: 您可以通过创建对
tcs
的显式强引用来避免过早的GC:
public Task SynchronizeAsync()
{
var gch = GCHandle.Alloc(tcs);
return tcs.Task.ContinueWith(
t => { gch.Free(); return t; },
TaskContinuationOptions.ExecuteSynchronously).Unwrap();
}
Or, a more readable version using async
: 或者,使用
async
的更易读的版本:
public async Task SynchronizeAsync()
{
var gch = GCHandle.Alloc(tcs);
try
{
await tcs.Task;
}
finally
{
gch.Free();
}
}
To take this research a bit further, consider the following little change, note Task.Delay(Timeout.Infinite)
and the fact that I return and use sync
as the Result
for Task<object>
. 为了进一步研究这个问题,请考虑以下一点变化,注意
Task.Delay(Timeout.Infinite)
以及我返回并使用sync
作为Task<object>
的Result
的事实。 It doesn't get any better: 它没有变得更好:
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, it's quite unexpected and undesirable that sync
object gets prematurely GC'ed before I could access it via task.Result
. IMO,在我通过
task.Result
访问它之前, sync
对象过早地被task.Result
是非常意外和不可取的 。
Now, change Task.Delay(Timeout.Infinite)
to Task.Delay(Int32.MaxValue)
and it all works as expected. 现在,将
Task.Delay(Timeout.Infinite)
更改为Task.Delay(Int32.MaxValue)
,它们都按预期工作。
Internally, it comes down to the strong reference on the await
continuation callback object (the delegate itself) which should be held while the operation resulting in that callback is still pending (in flight). 在内部,它归结为
await
continuation回调对象(委托本身)上的强引用,该对象应该在导致该回调的操作仍在等待(在飞行中)时保持。 I explained this in " Async/await, custom awaiter and garbage collector ". 我在“ Async / await,custom awaiter and garbage collector ”中解释了这一点。
IMO, the fact that this operation might be never-ending (like Task.Delay(Timeout.Infinite)
or incomplete TaskCompletionSource
) should not be affecting this behavior. IMO,这个操作可能永无止境的事实(如
Task.Delay(Timeout.Infinite)
或不完整的TaskCompletionSource
)不应该影响这种行为。 For most of naturally asynchronous operations, such strong reference is indeed held by the underlying .NET code which makes low-level OS calls (like in case with Task.Delay(Int32.MaxValue)
, which passes the callback to the unmanaged Win32 timer API and holds on to it with GCHandle.Alloc
). 对于大多数自然异步操作,这种强引用确实由底层.NET代码保存,后者生成低级OS调用(如
Task.Delay(Int32.MaxValue)
,它将回调传递给非托管Win32计时器API)并坚持使用GCHandle.Alloc
)。
In case there is no pending unmanaged calls on any level (which might be the case with Task.Delay(Timeout.Infinite)
, TaskCompletionSource
, a cold Task
, a custom awaiter), there is no explicit strong references in place, the state machine's object graph is purely managed and isolated , so the unexpected GC does happen. 如果在任何级别上没有挂起的非托管调用(可能是
Task.Delay(Timeout.Infinite)
, TaskCompletionSource
,冷Task
,自定义等待者)的情况,则没有明确的强引用, 状态机的对象图是纯粹管理和隔离的 ,因此意外的GC确实发生了。
I think this is a small design trade-off in async/await
infrastructure, to avoid making normally redundant strong references inside ICriticalNotifyCompletion::UnsafeOnCompleted
of standard TaskAwaiter
. 我认为这是
async/await
基础设施中的一个小设计权衡,以避免在标准TaskAwaiter
ICriticalNotifyCompletion::UnsafeOnCompleted
中进行通常冗余的强引用。
Anyhow, a possibly universal solution is quite easy to implement, using a custom awaiter (let's call it StrongAwaiter
): 总之,一个可能通用的解决方案很容易实现,使用自定义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;
}
StrongAwaiter
itself (generic and non-generic): 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;
}
}
}
async
state machine alive.
async
状态机活着的重要性。
The release build will crash if GCHandle.Alloc(tcs)
and gch.Free()
lines are commented out.
GCHandle.Alloc(tcs)
和gch.Free()
行,则发布版本将崩溃。
Either callback
or tcs
has to be pinned for it to work properly.
callback
或tcs
才能使其正常工作。
Alternatively, await tcs.Task.WithStrongAwaiter()
can be used instead, utilizing the above StrongAwaiter
.
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);
}
}
You think that you are still reference to Synchronizer, because you assume that your TaskCompletionSource is still reference to the Synchronizer and your TaskCompletionSource is still "alive" (referenced by GC roots). 你认为你仍然引用了Synchronizer,因为你假设你的TaskCompletionSource仍然是对Synchronizer的引用,你的TaskCompletionSource仍然是“活着的”(由GC根引用)。 One of these assumption is not right.
其中一个假设是不对的。
Now, forget about your TaskCompletionSource
现在,忘掉你的
TaskCompletionSource
replace the line 替换线
return tcs.Task;
by for example 例如
return Task.Run(() => { while (true) { } });
then you won't enter the Destructor ever again. 那么你将不会再次进入析构函数。
The conclusion is that: If you want to make sure that a object won't be garbage collected, then you will have to explicitly make a strong reference to it. 结论是:如果你想确保一个对象不会被垃圾收集,那么你必须明确地强烈引用它。 Don't assume that a object is "safe" because it is referenced by something not in your control.
不要认为对象是“安全的”,因为它是由不在您控制范围内的东西引用的。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.