Подтвердить что ты не робот

Почему GC собирает мой объект, когда у меня есть ссылка на него?

Посмотрим на следующий фрагмент, который показывает проблему.

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 собирает мой объект, когда у меня есть ссылка на него?

Исследование: Я потратил некоторое время на изучение того, что происходит за кулисами. Похоже, что конечная машина, сгенерированная компилятором С#, хранится как локальная переменная, а после первого удара await кажется, что сам конечный автомат выходит из сферы действия,

Таким образом, GC.KeepAlive(sync); и sync.Dispose(); не помогают, поскольку они живут внутри конечного автомата, где в качестве конечного автомата не существует.

Компилятор С# не должен генерировать код, из-за которого мой экземпляр sync выходит из области видимости, когда мне это все еще нужно. Это ошибка в компиляторе С#? Или я пропущу что-то фундаментальное?

PS: Я не ищу обходного пути, а скорее объяснение, почему компилятор делает это? Я googled, но не нашел каких-либо связанных вопросов, если это дублирует извините за это.

Update1: Я изменил создание TaskCompletionSource для хранения экземпляра Synchronizer, который по-прежнему не помогает.

4b9b3361

Ответ 1

Что GC.KeepAlive(sync) - который пустой сам - это просто инструкция для компилятора, чтобы добавить объект sync на конечный автомат struct, сгенерированный для Start. Как указывал @usr, внешняя задача, возвращаемая Start его вызывающей стороне, не содержит ссылки на этот внутренний конечный автомат.

С другой стороны, задача TaskCompletionSource tcs.Task, используемая внутри внутри Start, содержит такую ​​ссылку (поскольку она содержит ссылку на обратный вызов продолжения await и, следовательно, весь конечный автомат, обратный вызов регистрируется tcs.Task await внутри Start, создавая круговую ссылку между tcs.Task и конечным автоматом). Однако ни tcs, ни tcs.Task не отображается вне Start (где он мог бы быть сильным), поэтому граф объекта конечного авто изолирован и получает GC'ed.

Вы могли бы избежать преждевременного GC, создав явную сильную ссылку на 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 для 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 преждевременно получает GC'ed, прежде чем я могу получить к нему доступ через task.Result.

Теперь измените Task.Delay(Timeout.Infinite) на Task.Delay(Int32.MaxValue) и все будет работать как ожидалось.

Внутренне это сводится к сильной ссылке на объект обратного вызова продолжения await (сам делегат), который должен удерживаться во время операции, в результате чего обратный вызов все еще ожидает (в полете). Я объяснил это в Async/await, пользовательском awaiter и сборщике мусора.

IMO, тот факт, что эта операция может быть бесконечной (например, Task.Delay(Timeout.Infinite) или неполной TaskCompletionSource), не должна влиять на это поведение. Для большинства естественно асинхронных операций такая сильная ссылка действительно поддерживается базовым кодом .NET, который делает вызовы ОС низкого уровня (например, в случае Task.Delay(Int32.MaxValue)), который передает обратный вызов неуправляемому API-интерфейсу Win32 и удерживает его с GCHandle.Alloc).

В случае отсутствия ожидающих неуправляемых вызовов на любом уровне (что может быть в случае Task.Delay(Timeout.Infinite), TaskCompletionSource, холодного Task, пользовательского awaiter), нет явных сильных ссылок на месте, граф объекта конечных машин является чисто управляемым и изолированным, поэтому неожиданный GC происходит.

Я думаю, что это небольшой компромисс между дизайном в инфраструктуре async/await, чтобы избежать создания стандартных избыточных ссылок внутри ICriticalNotifyCompletion::UnsafeOnCompleted стандартного TaskAwaiter.

Во всяком случае, возможно универсальное решение довольно легко реализовать, используя пользовательский 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() закомментированы. Либо 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);
    }
}

Ответ 2

sync просто недоступен из любого GC-корня. Единственная ссылка на sync - это конечный автомат async. На этот государственный аппарат не ссылаются нигде. Несколько удивительно он не ссылается на Task или базовый TaskCompletionSource.

По этой причине sync конечный автомат и TaskCompletionSource мертвы.

Добавление GC.KeepAlive не предотвращает сбор. Это только предотвращает сбор, если ссылка на объект может действительно достигать этого утверждения.

Если я пишу

void F(Task t) { GC.KeepAlive(t); }

Тогда это не сохраняет ничего живого. Мне действительно нужно называть F чем-то (или он должен быть возможным для его вызова). Простое присутствие KeepAlive ничего не делает.

Ответ 3

Вы считаете, что вы по-прежнему ссылаетесь на Synchronizer, поскольку вы предполагаете, что ваш TaskCompletionSource по-прежнему ссылается на Synchronizer, а ваш TaskCompletionSource все еще "живой" (на который ссылаются корни GC). Одно из этих предположений неверно.

Теперь забудьте о своем TaskCompletionSource

замените строку

return tcs.Task;

например,

return Task.Run(() => { while (true) { } });

то вы больше не войдете в Destructor.

Вывод: Если вы хотите удостовериться, что объект не будет собирать мусор, вам придется явно указать на него сильную ссылку. Не предполагайте, что объект "безопасен", потому что на него ссылается что-то, что не находится под вашим контролем.