5

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

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

但我认为,我对今天在 CLR 中实现的“堆栈”和“堆”的理解是可靠的。例如,我不会做出一些不准确的概括性陈述,例如“值类型存储在堆栈上”。但是,在最常见的场景中——值类型的普通本地变量,或者作为参数传递,或者在方法中声明而不包含在闭包中——值类型变量存储在堆栈中(同样,在 Microsoft 的 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的范围内)存储在哪里?这是如何工作的?是盒装的吗?如果不是,尽管它是一种值类型,但它现在是否需要进行垃圾收集?还是可以立即回收内存?

我为这个冗长的问题道歉。但即使答案很简单,也许这对于那些发现自己在未来想知道同样事情的其他人来说是有用的。

4

3 回答 3

4

我不相信当你使用BeginInvoke()and EndInvoke()with reforout参数时,你真的是通过 ref传递变量。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 确实包含更新值。

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

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

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

我相信这个机制(在 Microsoft .NET 实现中)就是IAsyncResult对象。事实上,如果您IAsyncResult在调试器中检查对象,您会注意到在非公共成员中存在 exists _replyMsg,其中包含一个Properties集合。此集合包含类似的元素__OutArgs__Return其数据似乎反映了它们的同名。

编辑: 这是我想到的关于异步委托设计的理论。似乎BeginInvoke()和的签名EndInvoke()被选择为尽可能相似,以避免混淆并提高清晰度。该BeginInvoke()方法实际上不需要接受ref/out参数 - 因为它只需要它们的值......而不是它们的标识(因为它永远不会将任何东西分配给它们)。但是,(例如)有一个BeginInvoke()接受 an的电话int和一个接受 a 的电话真的很奇怪。现在,开始/结束调用应该具有相同的签名可能有技术原因 - 但我认为清晰和对称的好处足以验证这样的设计。EndInvoke()ref int

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

也许 C# 团队的某个人(如果他们看到这个问题)可以提供更多关于此功能背后的细节和设计选择的见解。

于 2010-10-13T14:45:10.380 回答
3

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

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

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

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

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

不太确定这个答案是否非常清楚,对 CPU 的工作原理和一点 C 知识有一些了解,因此指针的概念是水晶可以有很大帮助。

于 2010-10-13T14:26:03.200 回答
2

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

于 2010-10-13T14:23:31.757 回答