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

本文关键字:收集器 Await Async net | 更新日期: 2023-09-27 18:13:54

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

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

等待时会被垃圾收集吗?

也许。允许垃圾回收器这样做,但不要求这样做。

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

也许。允许垃圾回收器这样做,但不要求这样做。

基本上,如果垃圾收集器知道缓冲区不会再被触摸,那么它可以在任何时候释放它。但是从来没有要求GC 在任何特定的调度中释放任何东西。

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

await被实现为return,因此局部将超出作用域,其生命周期将结束;数组将在下一次收集中收集,这需要在Delay期间收集,对吗?

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

首先,如果任务没有完成,await只能是return;现在,当然几乎不可能完成Delay,所以,是的,它将返回,但我们通常不能得出await返回给调用者的结论。

第二,局部只有在c#编译器在IL中实际实现为临时池中的局部时才会消失。抖动将把它作为堆栈槽或寄存器,当方法的激活在await结束时,它就消失了。但是c#编译器不需要这样做!

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

第三,不需要垃圾收集器按照任何特定的调度收集任何东西。这个大数组将在大对象堆上分配,并且它有自己的收集计划。

第四,在任何给定的60秒间隔内,都不需要有一个大对象堆的集合。如果没有内存压力,则不需要收集该对象

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

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

更多注释:

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

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

public byte[] <Buffer>5__1;
因此,除非JIT和/或GC 真的聪明(详见Eric Lippert的回答),否则可以合理地假设,在异步任务完成之前,大型byte[]将保持在作用域内。

我很确定它是收集的,因为await结束当前任务并"继续"另一个任务,因此,当局部变量在await之后不再使用时,应该清理它们。

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

关于这个主题有一个关于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问题。

等待时会被垃圾收集吗?

不。

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

是的。

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