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

.NET 4.5 Async/Await и сборщик мусора

Мне интересно о поведении async/await в отношении сбора мусора локальных переменных. В следующем примере я выделил значительную часть памяти и зашел в значительную задержку. Как видно из кода, Buffer не используется после await. Будет ли сбор мусора собранным во время ожидания или память будет занята в течение всей функции?

/// <summary>
/// How does async/await behave in relation to managed memory?
/// </summary>
public async Task<bool> AllocateMemoryAndWaitForAWhile() {
    // Allocate a sizable amount of memory.
    var Buffer = new byte[32 * 1024 * 1024];
    // Show the length of the buffer (to avoid optimization removal).
    System.Console.WriteLine(Buffer.Length);
    // Await one minute for no apparent reason.
    await Task.Delay(60000);
    // Did 'Buffer' get freed by the garabage collector while waiting?
    return true;
}
4b9b3361

Ответ 1

Будет ли сбор мусора в ожидании?

Может быть. Сборщику мусора разрешено делать это, но не требуется.

Будет ли память занята в течение продолжительности функции?

Может быть. Сборщику мусора разрешено делать это, но не требуется.

В принципе, если сборщик мусора может знать, что буфер больше никогда не будет затронут, он может освободить его в любое время. Но GC никогда не требуется освобождать что-либо в любом конкретном расписании.

Если вы особенно обеспокоены, вы всегда можете установить локальный параметр null, но я бы не стал это делать, если у вас явно не было проблемы. В качестве альтернативы вы можете извлечь код, который манипулирует буфером, в свой собственный метод non-async и вызывать его синхронно из метода async; то локальный становится обычным локальным обычным методом.

await реализуется как return, поэтому локальный выход выходит из сферы действия и его время жизни будет закончено; массив будет затем собран в следующей коллекции, которая должна быть во время Delay, правильно?

Нет, ни одна из этих претензий не верна.

Во-первых, await - это только return, если задача не завершена; теперь, конечно, почти невозможно, что Delay будет завершено, так что да, это вернет, но мы не можем заключить вообще, что await возвращается к вызывающему.

Во-вторых, локальный только исчезает, если он фактически реализован в IL компилятором С# как локальный во временном пуле. Джиттер будет работать как слот стека или регистр, который исчезает, когда активация метода заканчивается на await. Но компилятор С# не обязан это делать!

Казалось бы странным, чтобы человек в отладчике поставил точку останова после Delay и увидел, что локаль исчезла, поэтому компилятор может реализовать локальное как поле в классе, генерируемом компилятором, который связан с время жизни класса, сгенерированного для конечного автомата. В этом случае гораздо менее вероятно, что джиттер поймет, что это поле никогда не читается снова и, следовательно, гораздо меньше шансов выбросить его раньше. (Хотя это разрешено, а также компилятору С# разрешено устанавливать для поля значение null от вашего имени, если оно может доказать, что вы закончили его использование. Опять же, это было бы странно для человека в отладчике, который неожиданно видит их местное значение изменения без аргумента apparant, но компилятору разрешено генерировать любой код, однопоточное поведение которого корректно.)

В-третьих, ничто не требует, чтобы сборщик мусора собирал что-либо по любому конкретному графику. Этот большой массив будет выделен на большой куче объекта, и у этой вещи есть свой собственный график сбора.

В-четвертых, ничто не требует, чтобы там была коллекция большой кучи объекта за любой заданный шестидесятисекундный интервал. Эта вещь никогда не должна собираться, если нет давления памяти.

Ответ 2

То, что сказал Эрик Липперт, верно: компилятор С# имеет довольно много возможностей для того, что IL должен генерировать для метода async. Итак, если вы спрашиваете, что спецификация говорит об этом, тогда ответ будет следующим: массив может иметь право на сбор во время ожидания, что означает, что он может быть собран.

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

Некоторые примечания:

  • Объект конечной машины на самом деле является struct, но он используется, хотя интерфейс он реализует, поэтому он ведет себя как ссылочный тип для сбора мусора.
  • Если вы действительно определяете, что факт, что массив не будет собран, является проблемой для вас, возможно, стоит установить local в null до await. Но в подавляющем большинстве случаев вам не о чем беспокоиться. Я, конечно, не говорю, что вы должны регулярно устанавливать locals на null до await.
  • Это очень подробная информация о реализации. Он может меняться в любое время, и разные версии компилятора могут вести себя по-другому.

Ответ 3

Ваш код компилирует (в моей среде: VS2012, С# 5,.NET 4.5, режим Release), чтобы включить структуру, которая реализует IAsyncStateMachine, и имеет следующее поле:

public byte[] <Buffer>5__1;

Таким образом, если JIT и/или GC не являются действительно умными (см. ответ Эрика Липперта для более подробной информации), было бы разумно предположить, что большой byte[] останется в области действия до завершения асинхронной задачи.

Ответ 4

Я уверен, что он собран, так как ждет завершения вашей текущей задачи и "продолжается" с другой задачей, поэтому локальные вары следует очищать, когда они не используются после ожидания.

НО: То, что на самом деле делает компилятор, может быть чем-то другим, поэтому я не буду зависеть от такого поведения.

Ответ 5

В этом разделе есть обновление с компилятором Rolsyn.

Выполнение следующего кода в Visual Studio 2015 Обновление 3 в конфигурации выпуска создает

True
False

Поэтому локальные жители собирают мусор.

    private static async Task MethodAsync()
    {
        byte[] bytes = new byte[1024];
        var wr = new WeakReference(bytes);

        Console.WriteLine(wr.Target != null);

        await Task.Delay(100);

        FullGC();

        Console.WriteLine(wr.Target != null);

        await Task.Delay(100);

    }

    private static void FullGC()
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();
    }

Обратите внимание, что если мы модифицируем MethodAsync для использования локальной переменной после ожидания, тогда буфер массива не будет собираться мусором.

 private static async Task MethodAsync()
    {
        byte[] bytes = new byte[1024];
        var wr = new WeakReference(bytes);

        Console.WriteLine(wr.Target != null);

        await Task.Delay(100);

        Console.WriteLine(bytes.Length);

        FullGC();

        Console.WriteLine(wr.Target != null);

        await Task.Delay(100);

        FullGC();

        Console.WriteLine(wr.Target != null);
    }

Выход для этого

True
1024
True
True

Образцы кода берутся из этого rolsyn issue.

Ответ 6

Будет ли сбор мусора в ожидании?

Неа.

будет ли память занята в течение продолжительности функции?

Да.

Откройте сборку в Reflector. Вы увидите, что компилятор сгенерировал частную структуру, которая наследует от IAsyncStateMachine, локальные переменные вашего метода async являются полем этой структуры. Поля данных класса/структуры никогда не освобождаются, пока владеющий экземпляр все еще жив.