让我们看看下面显示问题的片段。
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
反对状态机 struct
为 Start
生成.正如@usr 所指出的,Start
返回的外部 任务对其调用者的调用不包含对此内部状态机的引用。
另一方面,TaskCompletionSource
的 tcs.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()
行被注释掉。 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<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/