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

Dan*_*Tao 5 .net heap stack reference value-type

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

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

但是,我认为现在对CLR中实现的"堆栈"和"堆"的理解是可靠的.例如,我不打算做一些不准确的伞语句,例如"值类型存储在堆栈中".但是,在最常见的场景-普通的香草局部变量,值类型作为参数通过或声明的方法中,并没有包含在瓶盖内-值类型变量保存在栈上(同样,微软的CLR).

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

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

A() -> B() -> C()
Run Code Online (Sandbox Code Playgroud)

...然后在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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

LBu*_*kin 4

我不相信当您使用BeginInvoke()andEndInvoke()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'
}
Run Code Online (Sandbox Code Playgroud)

如果您现在检查输出,您会发现 的值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在调试器中检查该对象,您会注意到在非公共成员中存在_replyMsg,其中包含一个Properties集合。该集合包含类似 和 的元素__OutArgs__Return其数据似乎反映了它们的同名元素。

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

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

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