5

これが実装の詳細であることを理解しています。私は実際、その実装の詳細Microsoftの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);
}

したがって、上記の例では、xAのスコープ内で)どこに保存されていますか?そして、これはどのように機能しますか?箱入りですか?そうでない場合、値型であるにもかかわらず、ガベージコレクションの対象になりますか?または、メモリをすぐに回収できますか?

長い質問をお詫び申し上げます。しかし、答えが非常に単純であっても、これは、将来同じことを考えている他の人にとって有益なものになるかもしれません。

4

3 に答える 3

4

引数を使用したり、引数を使用したりすると、変数がBeginInvoke()本当にrefによって渡されるとは思いません。EndInvoke()refoutEndInvoke()パラメータを使用して呼び出す必要があるという事実は、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を調べた後、reftoの引数BeginInvoke()はまだ渡されているようby refです。ReflectorはこのメソッドのILを表示しませんが、パラメーターをref引数として渡さないだけで、代わりにに渡すための別の変数をバックグラウンドで作成するのではないかと思いますB()。次に呼び出すときは、非同期状態から値を取得するために引数を再度指定EndInvoke()する必要があります。このような引数は、最終的に値を取得するために必要なオブジェクトrefの一部として(またはオブジェクトと組み合わせて)実際に格納される可能性があります。IAsyncResult

動作がこのように機能する可能性が高い理由を考えてみましょう。メソッドを非同期呼び出しするときは、別のスレッドで呼び出します。このスレッドには独自のスタックがあるため、ref/out変数のエイリアシングの一般的なメカニズムを使用できません。ただし、非同期メソッドから戻り値を取得するには、最終的にを呼び出しEndInvoke()て操作を完了し、これらの値を取得する必要があります。ただし、への呼び出しEndInvoke()は、元の呼び出しとはまったく異なるスレッドで簡単に発生する可能性があります。BeginInvoke()またはメソッドの実際の本体。明らかに、呼び出しスタックはそのようなデータを格納するのに適した場所ではありません。特に、非同期操作が完了すると、非同期呼び出しに使用されるスレッドが別のメソッドに再利用される可能性があるためです。その結果、最終的に使用されるサイトにコールバックされるメソッドからの戻り値とout / ref引数を「マーシャリング」するには、スタック以外のメカニズムが必要になります。

このメカニズム(Microsoft .NET実装)がIAsyncResultオブジェクトだと思います。実際、IAsyncResultデバッガーでオブジェクトを調べると、非公開メンバーにコレクション_replyMsgを含むが存在することがわかります。Propertiesこのコレクションには、のような要素が含まれて__OutArgsおり、__Returnそのデータは同名の要素を反映しているように見えます。

編集: これが私に起こる非同期デリゲート設計についての理論です。との署名はBeginInvoke()EndInvoke()混乱を避け、明快さを向上させるために、互いに可能な限り類似するように選択された可能性があります。BeginInvoke()メソッドは実際には引数を受け入れる必要ref/outはありません-値だけが必要なので...IDは必要ありません(引数に何も割り当てられないため)。ただし、(たとえば)をBeginInvoke()とる呼び出しとをとる呼び出しがあるのintは本当に奇妙です。さて、開始/終了呼び出しが同一の署名を持つべきであるという技術的な理由がある可能性がありますが、そのような設計を検証するには、明快さと対称性の利点で十分だと思います。EndInvoke()ref int

もちろん、これはすべてCLRおよびC#コンパイラの実装の詳細であり、将来変更される可能性があります。BeginInvoke()ただし、渡された元の変数が実際に変更されることを期待している場合は、混乱する可能性があることは興味深いことです。またEndInvoke()、非同期操作を完了するために呼び出すことの重要性を強調しています。

おそらく、C#チームの誰か(この質問を見た場合)は、この機能の背後にある詳細と設計の選択についてより多くの洞察を提供することができます。

于 2010-10-13T14:45:10.380 に答える
3

CLRはこれに関して完全にループから外れています。参照によって渡される引数を取得するための適切なマシンコードを生成するのは、JITコンパイラの仕事です。これはそれ自体が実装の詳細であり、マシンアーキテクチャごとに異なるジッタがあります。

しかし、一般的なものは、Cプログラマーが行うのとまったく同じ方法でそれを行い、変数へのポインターを渡します。そのポインタは、メソッドが取る引数の数に応じて、CPUレジスタまたはスタックフレームで渡されます。

変数が存在するかどうかは関係ありませんが、呼び出し元のスタックフレーム内の変数へのポインターは、ヒープに格納されている参照型オブジェクトのメンバーへのポインターと同じように有効です。ガベージコレクターは、ポインター値によってそれらの違いを認識し、オブジェクトを移動するときに必要に応じてポインターを調整します。

コードスニペットは、あるスレッドから別のスレッドへのマーシャリング呼び出しを行うために必要な.NETFramework内の魔法を呼び出します。これは、Remotingを機能させるのと同じ種類の配管です。このような呼び出しを行うには、呼び出しが実行されるスレッドに新しいスタックフレームを作成する必要があります。リモーティングコードは、デリゲートの型定義を使用して、そのスタックフレームがどのように見えるかを認識します。そして、参照によって渡された引数を処理できます。これは、ポイントされた変数を格納するためにスタックフレームにスロットを割り当てる必要があることを認識しています。BeginInvoke呼び出しは、リモートスタックフレーム内のi変数のコピーを初期化します。

EndInvoke()呼び出しでも同じことが起こり、結果はスレッドプールスレッドのスタックフレームからコピーされます。重要な点は、実際にはi変数へのポインターではなく、そのコピーへのポインターがあるということです。

この答えが非常に明確であるかどうかはわかりません。CPUがどのように機能するかをある程度理解し、Cの知識を少し持っているので、ポインターの概念はクリスタルです。

于 2010-10-13T14:26:03.200 に答える
2

リフレクターで生成されたコードを見てください。私の推測では、クロージャ(現在のスタックフレーム内の変数を参照するラムダ式)を使用する場合のように、xを含む匿名クラスが生成されます。これを忘れて、他の答えを読んでください。

于 2010-10-13T14:23:31.757 に答える