在Microsoft的CLR中,为异步方法调用存储的ref值类型参数在哪里?

时间:2010-10-13 14:02:26

标签: .net reference stack heap value-type

我知道这是一个实现细节。我真的很好奇微软的CLR中 的实现细节是什么。

现在,请耐心等待我,因为我没有在大学学习CS,所以我可能错过了一些基本原则。

但是,我认为,对于今天在CLR中实现的“堆栈”和“堆”的理解是可靠的。例如,我不打算做一些不准确的伞语句,例如“值类型存储在堆栈中”。但是,在大多数常见场景中 - 值类型的普通vanilla局部变量,作为参数传递或在方法中声明而不包含在闭包内 - 值类型变量 存储在堆栈中(再次,在微软的CLR中。

我想我不确定的是ref值类型参数的来源。

最初我在想的是,如果调用堆栈看起来像这样(左=底):

A() -> B() -> C()

...然后在 A 范围内声明并作为ref参数传递给 B 的局部变量仍然可以存储在堆栈中 - - 不是吗? B 只需要将局部变量存储在 A 框架内的内存位置(如果这不是正确的术语,请原谅我;我认为我很明白我的内容无论如何,这意味着。

我意识到这不可能是严格正确的,但是,当我想到我能做到这一点时:

delegate void RefAction<T>(ref T arg);

void A()
{
    int x = 100;

    RefAction<int> b = B;

    // This is a non-blocking call; A will return immediately
    // after this.
    b.BeginInvoke(ref x, C, null);
}

void B(ref int arg)
{
    // Putting a sleep here to ensure that A has exited by the time
    // the next line gets executed.
    Thread.Sleep(1000);

    // Where is arg stored right now? The "x" variable
    // from the "A" method should be out of scope... but its value
    // must somehow be known here for this code to make any sense.
    arg += 1;
}

void C(IAsyncResult result)
{
    var asyncResult = (AsyncResult)result;
    var action = (RefAction<int>)asyncResult.AsyncDelegate;

    int output = 0;

    // This variable originally came from A... but then
    // A returned, it got updated by B, and now it's still here.
    action.EndInvoke(ref output, result);

    // ...and this prints "101" as expected (?).
    Console.WriteLine(output);
}

因此,在上面的示例中,x(在 A 的范围内)存储在哪里?这是如何工作的?盒装了吗?如果不是,它现在是否受垃圾收集,尽管它是一个值类型?或者可以立即回收记忆?

我为这个冗长的问题道歉。但即使答案很简单,也许这将为那些发现自己在未来想到同样事情的人提供信息。

3 个答案:

答案 0 :(得分:4)

我不相信当您使用BeginInvoke()EndInvoke()refout参数时,您真正传递参考的变量我们必须使用EndInvoke()参数调用ref这一事实应该是一个线索。

让我们改变你的例子来展示我描述的行为:

void A()
{
    int x = 100;
    int z = 400;

    RefAction<int> b = B;

    //b.BeginInvoke(ref x, C, null);
    var ar = b.BeginInvoke(ref x, null, null);
    b.EndInvoke(ref z, ar);

    Console.WriteLine(x);  // outputs '100'
    Console.WriteLine(z);  // outputs '101'
}

如果您现在检查输出,您会看到x的值实际上没有变化。但是z 现在包含更新值。

我怀疑当你使用异步的Begin / EndInvoke方法时,编译器会改变ref传递变量的语义。

在查看此代码生成的IL之后,ref的{​​{1}}个参数仍然传递BeginInvoke()。虽然Reflector没有显示此方法的IL,但我怀疑它不会将参数作为by ref参数传递,而是在幕后创建一个单独的变量以传递给ref 。然后,当您调用B()时,必须再次提供EndInvoke()参数以从异步状态检索值。这些参数实际上可能存储为最终检索其值所需的ref对象的一部分(或与之结合)。

让我们考虑为什么行为可能会以这种方式运行。当您对方法进行异步调用时,您是在单独的线程上执行此操作。该线程有自己的堆栈,因此不能使用别名IAsyncResult变量的典型机制。但是,为了从异步方法获取任何返回值,您需要最终调用ref/out来完成操作并检索这些值。但是,对EndInvoke()的调用可能很容易发生在与EndInvoke()的原始调用或方法的实际主体完全不同的线程上。显然,调用堆栈不是存储此类数据的好地方 - 特别是因为异步操作完成后,用于异步调用的线程可以重新用于不同的方法。因此,需要除堆栈之外的某些机制来“编组”返回值和out / ref参数,从被调用的方法返回到最终将使用它们的站点。

我相信这种机制(在Microsoft .NET实现中)是BeginInvoke()对象。实际上,如果您检查调试器中的IAsyncResult对象,您会注意到在非公共成员中存在IAsyncResult,其中包含_replyMsg集合。此集合包含Properties__OutArgs等元素,其数据似乎反映了他们的名字。

编辑: 以下是关于异步代理设计的理论,这种理论发生在我身上。 {{1}的签名似乎很可能选择{}和__Return尽可能相似,以避免混淆并提高清晰度。 BeginInvoke()方法实际上并不需要 来接受EndInvoke()个参数 - 因为它只需要它们的值...而不是它们的标识(因为它永远不会将任何内容分配回来他们)。但是,进行BeginInvoke()调用会花费ref/outBeginInvoke()调用int,这真的很奇怪(例如)。现在,技术原因可能是为什么开始/结束调用应该具有相同的签名 - 但我认为清晰度和对称性的好处足以验证这样的设计。

当然,所有这些都是CLR和C#编译器的实现细节,并且可能在将来发生变化。然而,有趣的是,如果您希望传递给EndInvoke()的原始变量实际上被修改,则存在混淆的可能性。它还强调了调用ref int来完成异步操作的重要性。

也许来自C#团队的人(如果他们看到这个问题)可以更深入地了解此功能背后的细节和设计选择。

答案 1 :(得分:3)

CLR完全脱离了循环,JIT编译器的工作是生成适当的机器代码以获取通过引用传递的参数。这本身就是一个实现细节,不同的机器架构存在不同的抖动。

但是常见的做法与C程序员完成的方式完全相同,它们传递指向变量的指针。该指针在CPU寄存器或堆栈帧中传递,具体取决于该方法采用的参数数量。

在变量生存无关紧要的情况下,指向调用者堆栈帧中变量的指针与指向存储在堆上的引用类型对象成员的指针一样有效。垃圾收集器通过指针值知道它们之间的区别,在移动对象时根据需要调整指针。

您的代码片段在.NET框架内调用魔法,这需要将调用从一个线程调用到另一个线程。这是使Remoting工作的同类管道。要进行这样的调用,必须在执行调用的线程上创建新的堆栈帧。远程处理代码使用委托的类型定义来知道堆栈框架应该是什么样子。它可以处理通过引用传递的参数,它知道它需要在堆栈帧中分配一个槽来存储指向变量 i 。 BeginInvoke调用初始化远程堆栈帧中 i 变量的副本。

在EndInvoke()调用中发生同样的事情,结果从线程池线程中的堆栈帧复制回来。关键点是实际上没有指向 i 变量的指针,有一个指向它的副本的指针。

不太确定这个答案非常明确,对CPU如何工作以及一些C知识有所了解所以指针的概念是水晶可以帮助很多。

答案 2 :(得分:2)

查看使用反射器生成的代码以查找。 我的猜测是生成一个包含x的匿名类,就像使用闭包(lambda表达式引用当前堆栈框架中的变量)一样。 忘记这一点并阅读其他答案。