20

我想知道async/await与垃圾收集局部变量有关的行为。在下面的示例中,我分配了相当大的内存部分并进入了显着延迟。从代码中可以看出,Bufferawait. 它会在等待时被垃圾收集,还是在函数执行期间内存被占用?

/// <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;
}
4

6 回答 6

26

它会在等待时收集垃圾吗?

也许。垃圾收集器可以这样做,但不是必须的。

内存会在函数执行期间被占用吗?

也许。垃圾收集器可以这样做,但不是必须的。

基本上,如果垃圾收集器可以知道缓冲区永远不会再被触及,那么它可以随时释放它。但是 GC 从来不需要在任何特定的时间表上释放任何东西。

如果您特别担心,您可以随时将 local 设置为null,但除非您明显有问题,否则我不会这样做。或者,您可以将操作缓冲区的代码提取到它自己的非异步方法中,并从异步方法同步调用它;那么本地就变成了普通方法的普通本地。

await实现为 a return,因此本地将超出范围并且其生命周期将结束;然后将在下一次收集时收集该数组,这需要在 期间Delay,对吗?

不,这些说法都不是真的。

首先,awaitreturn当任务未完成时,an 才为 a;现在,这当然几乎不可能Delay完成,所以是的,这将返回,但我们一般不能断定 anawait返回给调用者。

其次,只有当 C# 编译器在 IL 中将其实际实现为临时池中的本地时,本地才会消失。抖动会将其作为堆栈槽或寄存器进行抖动,当方法的激活在await. 但是 C# 编译器不需要这样做!

Delay对于调试器中的人来说,在 之后放置断点并看到本地已消失似乎很奇怪,因此编译器可能会将本地视为编译器生成的类中的一个字段,该字段绑定到生成的类的生命周期为状态机。在这种情况下,抖动将不太可能意识到该字段不再被读取,因此不太可能提前将其丢弃。(尽管允许这样做。如果 C# 编译器可以证明您已完成使用它,则允许C# 编译器代表您将该字段设置为 null。同样,这对于调试器中的人来说会很奇怪突然无缘无故地看到它们的本地更改值,但允许编译器生成任何单线程行为正确的代码。)

第三,没有什么要求垃圾收集器按任何特定的时间表收集任何东西。这个大数组将分配在大对象堆上,而那个东西有自己的收集时间表。

第四,没有什么要求在任何给定的 60 秒间隔内有一个大型对象堆的集合。如果没有内存压力,就永远不需要收集那个东西。

于 2013-05-16T22:40:38.543 回答
7

Eric Lippert 说的是真的:C# 编译器对于它应该为方法生成什么 IL 有很大的余地async。因此,如果您要问规范对此有何规定,那么答案是:该数组可能有资格在等待期间进行收集,这意味着它可能会被收集。

但另一个问题是编译器实际上做了什么。在我的计算机上,编译器生Buffer成为生成状态机类型的字段。该字段设置为分配的数组,然后再也不会设置。这意味着当状态机对象这样做时,该数组将有资格被收集。并且该对象是从延续委托中引用的,因此在等待完成之前它不会有资格被收集。这一切意味着该数组在等待期间将没有资格被收集,这意味着它不会被收集。

还有一些注意事项:

  1. 状态机对象实际上是一个struct,但它是通过它实现的接口来使用的,所以它作为一个引用类型来进行垃圾收集。
  2. 如果您确实确定不会收集数组这一事实对您来说是个问题,则可能值得将 local 设置nullawait. 但在绝大多数情况下,您不必担心这一点。我当然不是说您应该定期将 locals 设置为nullbefore await
  3. 这在很大程度上是一个实现细节。它可以随时更改,不同版本的编译器可能会有不同的行为。
于 2013-05-16T23:34:53.590 回答
3

您的代码编译(在我的环境中:VS2012、C# 5、.NET 4.5、发布模式)以包含一个实现 的结构IAsyncStateMachine,并具有以下字段:

public byte[] <Buffer>5__1;

因此,除非 JIT 和/或 GC真的很聪明,(有关更多信息,请参阅Eric Lippert 的回答),假设大型byte[]将保持在范围内直到异步任务完成是合理的。

于 2013-05-16T23:20:13.173 回答
0

我很确定它已被收集,因为 await 结束了您当前的任务并“继续”另一个任务,因此当本地变量在 await 之后不使用时应该被清理。

但是:编译器实际上所做的可能会有所不同,所以我不会依赖这种行为。

于 2013-05-16T22:49:08.227 回答
0

Rolsyn 编译器对此主题进行了更新。

在发布配置中的 Visual Studio 2015 Update 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();
    }

注意,如果我们在 await 之后修改 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问题

于 2016-11-03T08:44:16.903 回答
-2

它会在等待时收集垃圾吗?

没有。

内存会在函数执行期间被占用吗?

是的。

在 Reflector 中打开已编译的程序集。您将看到编译器生成了一个继承自 IAsyncStateMachine 的私有结构,异步方法的局部变量是该结构的一个字段。当拥有的实例仍然存在时,类/结构的数据字段永远不会被释放。

于 2013-05-16T23:19:48.800 回答