让我们看一下显示问题的以下片段。
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:我不是在寻找解决方法,而是解释为什么编译器会这样做?我用谷歌搜索,但没有发现任何相关问题,如果它是重复的抱歉。
更新1:我已修改TaskCompletionSource
创建以保留Synchronizer
实例,但仍无效。
答案 0 :(得分:8)
sync
。对sync
的唯一引用来自async
状态机。该状态机不会从任何地方引用。有点令人惊讶it is not referenced from the Task
or the underlying TaskCompletionSource
.
因此sync
,状态机和TaskCompletionSource
已经死了。
添加GC.KeepAlive
并不会阻止收集。如果对象引用实际上可以到达此语句,它只会阻止收集。
如果我写
void F(Task t) { GC.KeepAlive(t); }
然后这不会让任何保持活着。我实际上需要用某些东西调用F
(或者必须可以调用它)。只有KeepAlive
的存在不起作用。
答案 1 :(得分:7)
GC.KeepAlive(sync)
- 这是blank by itself - 这里只是指示编译器将sync
对象添加到为{{1}生成的状态机struct
}}。正如@usr指出的那样,Start
向其调用者返回的外部任务不包含对此内部状态机的引用
另一方面,Start
内部使用的TaskCompletionSource
tcs.Task
任务确实包含此类引用(因为它包含对{{1}的引用继续回调,从而整个状态机;回调在Start
await
内注册tcs.Task
,在await
和状态机之间创建循环引用。但是,Start
和tcs.Task
都没有公开在 tcs
之外(它可能是强引用的),因此状态机的对象图被隔离并获得GC。
您可以通过创建对tcs.Task
的明确强引用来避免过早的GC:
Start
或者,使用tcs
的更易读的版本:
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
IMO,在我通过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();
}
对象过早地被GC过期,这是非常意外和不受欢迎的。
现在,将sync
更改为task.Result
,这一切都按预期工作。
在内部,它归结为Task.Delay(Timeout.Infinite)
延续回调对象(委托本身)上的强引用,当导致该回调的操作仍然未决(在飞行中)时应该保持该引用。我在&#34; Async/await, custom awaiter and garbage collector&#34;。
IMO,这个操作可能永无止境的事实(如Task.Delay(Int32.MaxValue)
或不完整的await
)不应该影响这种行为。对于大多数自然异步操作,这种强引用确实由底层.NET代码所持有,后者生成低级OS调用(如Task.Delay(Timeout.Infinite)
的情况,它将回调传递给非托管Win32计时器API并保持不变TaskCompletionSource
)。
如果任何级别上没有待处理的非托管呼叫(可能是Task.Delay(Int32.MaxValue)
,GCHandle.Alloc
,冷Task.Delay(Timeout.Infinite)
,自定义等待的情况),则没有明确的强大的引用,状态机的对象图是纯粹的管理和隔离,因此意外的GC确实发生了。
我认为这是TaskCompletionSource
基础架构中的一个小设计权衡,以避免在标准Task
的{{1}}内进行正常冗余强引用。< / p>
无论如何,一个可能通用的解决方案很容易实现,使用自定义awaiter(让我们称之为async/await
):
ICriticalNotifyCompletion::UnsafeOnCompleted
TaskAwaiter
本身(通用和非通用):
StrongAwaiter
<小时/> 更新,这是一个真实的Win32互操作示例,说明了保持
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;
}
}
}
行,则发布版本将崩溃。必须固定async
或GCHandle.Alloc(tcs)
才能使其正常工作。或者,也可以使用gch.Free()
代替callback
。
tcs
答案 2 :(得分:1)
您认为您仍然引用了Synchronizer,因为您假设您的TaskCompletionSource仍然是对Synchronizer的引用,并且您的TaskCompletionSource仍然是“活着的”(由GC根引用)。其中一个假设是不对的。
现在,忘掉你的TaskCompletionSource
替换
行return tcs.Task;
例如
return Task.Run(() => { while (true) { } });
然后你不会再次进入析构函数。
结论是: 如果要确保不会对对象进行垃圾回收,则必须明确强制引用它。不要认为对象是“安全的”,因为它是由你无法控制的东西引用的。