【问题标题】:.NET 4.5 Async/Await and the Garbage Collector.NET 4.5 Async/Await 和垃圾收集器
【发布时间】:2016-11-03 16:44:16
【问题描述】:

我想知道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;
}

【问题讨论】:

  • 这取决于编译器如何翻译它。 async-await 不是 CLR 概念。 (不过,这是一个有效的问题)。
  • 很好的例子,错误的答案如何获得最多的支持。
  • @Soonts:您认为我的回答不正确,我感到有些好笑。我向您保证,这是对 C# 语言做出的保证 的正确陈述。其他任何内容都是可能随时更改的实现细节。
  • @EricLippert,OP 的问题不是理论上的。他不是在问“C# 语言能保证什么”。他宁愿问“它是如何工作的”。当然,实现细节可能会随着下一个 Visual Studio 甚至是服务包而改变。在这种情况下,正确答案将从“否”变为“是”。然而在此之前,在 Visual Studio 2012 的当前生产版本中,正确答案不是“也许”,而是“否”。
  • @soonts async void m(){ var x = new X();等待 n(x.h); } h 是一个句柄, ~X() 释放它。我问你析构函数是否可以在任务完成之前运行。如果是这样,则该过程会破坏数据库。你的回答仍然是因为终结器无法运行,所以这是完全安全的吗?现在你明白为什么重要的是要考虑什么是有保证的,什么是有效的吗?使用良好的工程实践,不要依赖实施细节。

标签: c# .net garbage-collection async-await


【解决方案1】:

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

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

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

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

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

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

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

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

首先,如果任务未完成,await 只是return;现在,Delay 将完成当然几乎是不可能的,所以是的,这将返回,但我们一般不能断定 await 返回给调用者。

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

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

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

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

【讨论】:

  • No转为也许,是还是不是?或者规则是什么?
  • @IV4:我第一次看错了代码,但很快就意识到我的错误。
  • if the garbage collector can know that the buffer will never be touched again then it can free it at any time GC 知道await Task.Delay(60000); 吗?
  • “为状态机生成的委托的生命周期” - 异步状态机不是委托,它是实现 IAsyncStateMachine 接口的结构。
  • @Soonts:你当然是对的。当我们设计这个特性时,它会是一个委托,但是当它发布时,它就有了一个接口。对于这个问题,编译器生成的类的确切形状是什么并不是特别重要。
【解决方案2】:

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

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

还有一些注意事项:

  1. 状态机对象实际上是一个struct,但它是通过它实现的接口来使用的,因此它作为一个引用类型来进行垃圾回收。
  2. 如果您确实确定不会收集数组这一事实对您来说是个问题,那么在await 之前将本地设置为null 可能是值得的。但在绝大多数情况下,您不必担心这一点。我当然不是说你应该在 await 之前定期将 locals 设置为 null
  3. 这是一个非常详细的实现细节。它可以随时更改,不同版本的编译器可能表现不同。

【讨论】:

  • 好点。我要补充一点,在设计阶段进行了一些讨论,以在编译时检测这些情况并自动清空字段,从而使垃圾收集器的生活更轻松(而开发人员在调试器中查看本地变量时更加困难)神秘地变成了null。)我不知道决定了什么。
【解决方案3】:

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

public byte[] <Buffer>5__1;

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

【讨论】:

  • 我认为这并不能真正回答问题,您还需要知道:1.编译器不是将字段设置为null吗? 2. state machine struct(它实际上不是一个类)什么时候可以被收集?
  • @svik,(1)不,它没有,编译使用Reflector验证(2)async方法完成后。
【解决方案4】:

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

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

【讨论】:

  • 我已经在我的回答中解决了你的假设。
【解决方案5】:

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 issue

【讨论】:

    【解决方案6】:

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

    没有。

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

    是的。

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

    【讨论】:

    • 嗯,这只是用另一个问题代替了一个问题:拥有的实例何时会停止存活?
    • 方法完成后。在此之前,任务调度程序持有在超时后调用 MoveNext 方法的引用。如果您想提前取消,请将 CancelationTocken 传递给 Task.Delay。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-04-20
    • 2012-07-14
    • 2010-10-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多