【问题标题】:How do closures work behind the scenes? (C#)闭包在幕后是如何工作的? (C#)
【发布时间】:2010-12-28 01:40:51
【问题描述】:

我觉得我对闭包、如何使用它们以及它们何时有用的理解相当不错。但我不明白的是它们在内存中的幕后实际工作方式。一些示例代码:

public Action Counter()
{
    int count = 0;
    Action counter = () =>
    {
        count++;
    };

    return counter;
}

通常,如果 {count} 没有被闭包捕获,它的生命周期将被限定为 Counter() 方法,并且在它完成后,它将与 Counter() 的其余堆栈分配一起消失。但是当它关​​闭时会发生什么?这个 Counter() 调用的整个堆栈分配是否仍然存在?它是否将 {count} 复制到堆中?它是否实际上从未在堆栈上分配,但被编译器识别为已关闭并因此始终存在于堆上?

对于这个特定的问题,我主要感兴趣的是它在 C# 中的工作原理,但不反对与支持闭包的其他语言进行比较。

【问题讨论】:

  • 好问题。我不确定,但是是的,您可以在 C# 中保留堆栈框架。生成器一直使用它(用于数据结构的 LINQ),它依赖于引擎盖下的产量。希望我没有偏离标准。如果我是,我会学到很多东西。
  • yield 将该方法转换为具有状态机的单独类。堆栈本身没有保留,但堆栈状态在编译器生成的类中移动到类状态
  • @thecoop,你有解释这个的链接吗?
  • 当然,如果您想了解如何构建迭代器,请阅读本系列:blogs.msdn.com/oldnewthing/archive/2008/08/12/8849519.aspx
  • 您绝对不能“保留堆栈框架”。堆栈帧在堆栈上!如果我们让堆栈保持活动状态,我们将如何弹出堆栈?

标签: c# .net closures


【解决方案1】:

你的第三个猜测是正确的。编译器会生成如下代码:

private class Locals
{
  public int count;
  public void Anonymous()
  {
    this.count++;
  }
}

public Action Counter()
{
  Locals locals = new Locals();
  locals.count = 0;
  Action counter = new Action(locals.Anonymous);
  return counter;
}

有意义吗?

另外,您要求进行比较。 VB 和 JScript 都以几乎相同的方式创建闭包。

【讨论】:

  • 既然 .NET 可以更好地处理 ref struct,那么当编译器可以证明闭包的生命周期时,闭包现在会使用零分配结构而不是类作为闭包吗?
  • @Dai:很好的问题,我不知道答案。回到我在微软的时候——回想一下我在 2012 年离开的时候——我们有很多关于改善闭包生命周期的想法,但我不知道是否有任何实现。
【解决方案2】:

编译器(相对于运行时)创建另一个类/类型。带有您的闭包的函数以及您关闭/提升/捕获的任何变量都将在您的代码中作为该类的成员重新编写。 .Net 中的闭包被实现为这个隐藏类的一个实例。

这意味着您的 count 变量完全是不同类的成员,并且该类的生命周期与任何其他 clr 对象一样工作;在它不再植根之前,它没有资格进行垃圾收集。这意味着只要您有对该方法的可调用引用,它就不会去任何地方。

【讨论】:

  • 使用 Reflector 检查有问题的代码以查看此示例
  • ...只需在您的解决方案中寻找最丑陋的命名类。
  • 这是否意味着关闭会导致新的堆分配,即使被关闭的值是原始值?
  • @Matt - 我不会称它为“新”,因为就生成的代码而言,您的原语总是在堆栈上。所需的闭包是在创建将使用该闭包的任何对象的同时创建的。
  • s/总是在栈上/总是在堆上/
【解决方案3】:

感谢@HenkHolterman。由于 Eric 已经解释过了,我添加了链接只是为了显示编译器为关闭生成的实际类。我想补充一点,C# 编译器创建显示类会导致内存泄漏。例如,在一个函数内部,有一个由 lambda 表达式捕获的 int 变量,还有一个简单地保存对大字节数组的引用的局部变量。编译器将创建一个显示类实例,该实例将保存对变量的引用,即 int 和字节数组。但是在引用 lambda 之前,不会对字节数组进行垃圾收集。

【讨论】:

    【解决方案4】:

    Eric Lippert 的回答确实切中要害。但是,最好构建一张关于堆栈帧和捕获通常如何工作的图片。要做到这一点,看一个稍微复杂一点的例子会有所帮助。

    这里是捕获代码:

    public class Scorekeeper { 
       int swish = 7; 
    
       public Action Counter(int start)
       {
          int count = 0;
          Action counter = () => { count += start + swish; }
          return counter;
       }
    }
    

    这就是我认为的等价物(如果我们幸运的话,Eric Lippert 会评论这是否真的正确):

    private class Locals
    {
      public Locals( Scorekeeper sk, int st)
      { 
          this.scorekeeper = sk;
          this.start = st;
      } 
    
      private Scorekeeper scorekeeper;
      private int start;
    
      public int count;
    
      public void Anonymous()
      {
        this.count += start + scorekeeper.swish;
      }
    }
    
    public class Scorekeeper {
        int swish = 7;
    
        public Action Counter(int start)
        {
          Locals locals = new Locals(this, start);
          locals.count = 0;
          Action counter = new Action(locals.Anonymous);
          return counter;
        }
    }
    

    关键是本地类替代了整个堆栈帧,并在每次调用 Counter 方法时进行相应的初始化。通常,堆栈帧包括对“this”的引用、方法参数和局部变量。 (当进入控制块时,栈帧实际上也被扩展了。)

    因此,我们不会只有一个对象对应于捕获的上下文,而是实际上每个捕获的堆栈帧都有一个对象。

    基于此,我们可以使用以下思维模型:堆栈帧保存在堆上(而不是堆栈上),而堆栈本身只包含指向堆上的堆栈帧的指针。 Lambda 方法包含一个指向堆栈帧的指针。这是使用托管内存完成的,因此框架会一直停留在堆上,直到不再需要为止。

    显然,当需要堆对象支持 lambda 闭包时,编译器可以通过仅使用堆来实现这一点。

    我喜欢这个模型的地方在于它为“收益回报”提供了综合图景。我们可以将迭代器方法(使用yield return)想象成它的堆栈帧是在堆上创建的,并且引用指针存储在调用者的局部变量中,以供迭代期间使用。

    【讨论】:

    • 不正确;如何从外部类Scorekeeper 访问私有swish?如果start 发生突变会发生什么?但更重要的是:用公认的答案回答一个八年前的问题有什么价值?
    • 如果你想知道真正的 codegen 是什么,请使用 ILDASM 或 IL-to-source 反汇编程序。
    • 完全考虑它的更好方法是停止将“堆栈框架”视为基本事物。堆栈只是一个数据结构,用于实现两件事:activationcontinuation。即:与激活方法相关的值是什么,该方法返回后将运行什么代码?但是堆栈只是一个合适的数据结构,用于存储激活/继续信息如果方法激活生命周期在逻辑上形成堆栈
    • 由于 lambdas、迭代器块和异步都启用了在逻辑上不形成堆栈的方法激活生命周期,因此堆栈不能用作激活和延续的数据结构。所以数据结构必须分配在长期池中
    • 您在激活和延续方面的 cmets 是有意义的。但是,激活通常发生在现有帧中,因此对帧有某种隐含的顺序。事实是,较早的帧可能会终止,而较晚的帧可能会持续更长的时间,因此存在间隙。另外我想我们也可以有异步硬件事件生成的帧。至于为什么要回答一个老问题,嗯,我从你一分钟前刚刚发布的内容中得到了价值:-)。我想我应该对您的答案发表评论,而不是重新开始。
    猜你喜欢
    • 1970-01-01
    • 2010-09-24
    • 1970-01-01
    • 2019-04-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-06-16
    相关资源
    最近更新 更多