通过延迟执行保留超出范围的堆栈变量

本文关键字:堆栈 变量 范围 执行 保留 通过延迟 | 更新日期: 2023-09-27 18:36:52

我承认这个问题显示出很多关于堆栈和堆栈帧的误解。希望我在这里问一个适当的问题。

在 C# 中,如何处理堆栈帧和代码的局部变量,如下所示。使这些情况有趣的是,StartTasks 中的变量 j 和 StartTasks2 中的变量 i 都由任务使用,这些任务很可能在这些变量不再在范围内运行,并且在运行它们的堆栈帧通常已从堆栈中弹出之后。

此外,在什么情况下,像 j 这样不断被"重新创建"的局部变量在超出范围后会获得一个全新的内存插槽,就像在 StartTask 中发生的那样,该局部变量位于何处(即 StartTasks 的堆栈帧,这意味着帧无法删除,或其他地方)?

void StartTasks() {
    int i = 0;
    while ( i < 10000 ) {
        int j = i;
        Task.Run( () => ExecuteThis( j ) ); // eac
    }
}
void StartTasks2() {
    int i = 0;
    while ( i < 10000 ) {
        Task.Run( () => ExecuteThis( i ) ); // eac
    }
}

void BigBoss() {
    StartTasks();
    StartTasks2();
    NowMakeMoreCalls();
}

通过延迟执行保留超出范围的堆栈变量

它们被提升为编译器生成的类的一部分。

对您提供的内容进行简单的反编译给出了一些答案:

[CompilerGenerated]
private sealed class <>c__DisplayClass5
{
  public int i;
  public Program <>__this;
  public <>__DisplayClass5()
  {
    base.<>ctor();
  }
  public void <CStartTasks2>b__3()
  {
    this.<>4__this.ExecuteThis(this.i);
  }
}

编译器生成此类,并将对调用类的引用传递给该类。它还将闭合的 over 变量存储为实例字段。

所以,为了回答这个问题......它们没有在堆栈上分配。它们形成一个在编译时定义并在运行时实例化的对象。

这个问题显示了对堆栈和堆栈帧的许多可能的误解。

你的理解很好,但你只是没有把所有的事实放在一起来得出解释。你已经完成了90%的路程。

堆栈帧是一个实现细节。不需要将本地实现为堆栈插槽。请记住,使本地成为本地的并不是它在堆栈上。它们被称为局部变量,而不是堆栈变量。使本地成为本地的原因是它的名称仅在方法内部有意义。

使这些情况有趣的是,StartTasks 中的变量 j 和 StartTasks2 中的变量 i 都由任务使用,这些任务很可能在这些变量不再在范围内运行,并且在运行它们的堆栈帧通常已从堆栈中弹出之后。

首先,您错误地使用了术语"范围"。"范围"是一个编译时概念;局部变量的作用域是代码文本的区域,可通过其名称访问该变量。 您使用"范围"作为堆栈帧生存期的运行时概念。这不是范围;那是一辈子。

您正确地注意到,局部变量的生存期比堆栈帧的生存期长。显然,这意味着本地不能实现为堆栈插槽。 其实不然。该局部被实现为一个领域。字段是任务引用的对象字段。

什么条件下,像J这样不断被"重新创建"的局部变量在超出范围后会得到一个全新的内存槽

重复使用插槽时会创建一个不正确的程序!重用插槽是一种优化。编译器不会进行创建错误程序的优化。

该局部变量位于何处?

如果已知变量的生存期与堆栈帧的生存期相同(或更短),则可以继续堆栈。如果没有,那么它必须继续堆。