![](/img/trans.png)
[英]does garbage collector set the reference to null when it collects an object?
[英]Why does GC collects my object when I have a reference to it?
讓我們看一下顯示問題的以下片段。
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:我不是在尋找解決方法,而是解釋為什么編譯器會這樣做? 我用Google搜索,但沒有找到任何相關的問題,如果它是重復的抱歉。
Update1:我已經修改了TaskCompletionSource
創建以保存Synchronizer
實例,但仍無法提供幫助。
無法從任何GC根目錄訪問sync
。 唯一的sync
參考來自async
狀態機。 該狀態機不會從任何地方引用。 有點令人驚訝的是它沒有從Task
或底層的TaskCompletionSource
。
出於這個原因, sync
,狀態機和TaskCompletionSource
已經死了。
添加GC.KeepAlive
不會阻止自身收集。 如果對象引用實際上可以到達此語句,它只會阻止收集。
如果我寫
void F(Task t) { GC.KeepAlive(t); }
然后,這不會保持任何活力。 我實際上需要用某些東西調用F
(或者必須可以調用它)。 只有KeepAlive
存在什么都不做。
什么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<object>
的Result
的事實。 它沒有變得更好:
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,在我通過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
,自定義等待者)的情況,則沒有明確的強引用, 狀態機的對象圖是純粹管理和隔離的 ,因此意外的GC確實發生了。
我認為這是async/await
基礎設施中的一個小設計權衡,以避免在標准TaskAwaiter
ICriticalNotifyCompletion::UnsafeOnCompleted
中進行通常冗余的強引用。
總之,一個可能通用的解決方案很容易實現,使用自定義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
本身(通用和非通用):
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
才能使其正常工作。
或者,可以使用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);
}
}
你認為你仍然引用了Synchronizer,因為你假設你的TaskCompletionSource仍然是對Synchronizer的引用,你的TaskCompletionSource仍然是“活着的”(由GC根引用)。 其中一個假設是不對的。
現在,忘掉你的TaskCompletionSource
替換線
return tcs.Task;
例如
return Task.Run(() => { while (true) { } });
那么你將不會再次進入析構函數。
結論是:如果你想確保一個對象不會被垃圾收集,那么你必須明確地強烈引用它。 不要認為對象是“安全的”,因為它是由不在您控制范圍內的東西引用的。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.