.NET 4.5 Async / Await和垃圾收集器

时间:2013-05-16 22:16:39

标签: c# .net garbage-collection 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;
}

6 个答案:

答案 0 :(得分:25)

  

等待时会收集垃圾吗?

也许。垃圾收集器是允许这样做的,但不是必需的。

  

在函数持续时间内是否会占用内存?

也许。垃圾收集器是允许这样做的,但不是必需的。

基本上,如果垃圾收集器可以知道缓冲区永远不会被再次触摸,那么它可以随时释放它。但GC绝不会要求在任何特定时间表上免费提供任何内容。

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

  

await被实现为return,因此本地将超出范围,其生命周期将结束;然后将在下一个集合上收集数组,该集合必须在Delay期间,对吧?

不,这些说法都不属实。

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

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

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

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

第四,没有任何东西要求在任何给定的60秒间隔内存在大对象堆的集合。如果没有记忆压力,就不需要收集那件东西。

答案 1 :(得分:7)

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

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

更多说明:

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

答案 2 :(得分:2)

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

public byte[] <Buffer>5__1;

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

答案 3 :(得分:0)

我很确定它的收集,因为等待结束你当前的任务并“继续”另一项任务, 因此,在等待之后不使用本地变量时应该清理它们。

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

答案 4 :(得分: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();
    }

注意,如果我们修改MethodAsync以在await之后使用局部变量,那么数组缓冲区将不会被垃圾收集。

 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

答案 5 :(得分:-2)

  

等待时会收集垃圾吗?

不。

  

将在函数持续时间内占用内存吗?

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