簡體   English   中英

為什么GC在我引用它時會收集我的對象?

[英]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內部使用的TaskCompletionSourcetcs.Task任務確實包含這樣的引用(因為它包含對await continuation回調的引用,因此包含對整個狀態機的引用;回調是在tcs.Task注冊時注冊的在Start內部await ,在tcs.Task和狀態機之間創建一個循環引用。 但是, tcstcs.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;
        }
    }
}


更新 ,這是一個真實的Win32互操作示例,說明了保持async狀態機活着的重要性。 如果GCHandle.Alloc(tcs)gch.Free()行,則發布版本將崩潰。 必須固定callbacktcs才能使其正常工作。 或者,可以使用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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM