我想知道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;
}
答案 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委托引用的,因此在等待完成之后它才有资格进行收集。这一切意味着数组不会在等待期间有资格收集,这意味着它不会被收集。
更多说明:
struct
,但是它通过它实现的接口使用,所以它的行为就像垃圾收集一样。null
之前将本地设置为await
可能是值得的。但在绝大多数情况下,您不必担心这一点。我当然不是说你应该在null
之前定期将本地人设置为await
。答案 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的私有结构,异步方法的局部变量是该结构的字段。当拥有的实例仍然存活时,类/结构的数据字段永远不会被释放。