15

この質問に見られるように: 拡張メソッドを使用してC#イベントを発生させる-それは悪いですか?

この拡張メソッドを使用してイベントを安全に発生させることを考えています。

public static void SafeRaise(this EventHandler handler, object sender, EventArgs e)
{
    if (handler != null)
        handler(sender, e);
}

しかし、Mike Rosenblumは、JonSkeetの回答でこの懸念を提起しています。

[MethodImpl(MethodImplOptions.NoInlining)]属性をこれらの拡張メソッドに追加する必要があります。そうしないと、デリゲートを一時変数にコピーする試みがJITterによって最適化され、null参照例外が発生する可能性があります。

拡張メソッドがNoInliningでマークされていない場合に競合状態が発生するかどうかを確認するために、リリースモードでいくつかのテストを行いました。

int n;
EventHandler myListener = (sender, e) => { n = 1; };
EventHandler myEvent = null;

Thread t1 = new Thread(() =>
{
    while (true)
    {
        //This could cause a NullReferenceException
        //In fact it will only cause an exception in:
        //    debug x86, debug x64 and release x86
        //why doesn't it throw in release x64?
        //if (myEvent != null)
        //    myEvent(null, EventArgs.Empty);

        myEvent.SafeRaise(null, EventArgs.Empty);
    }
});

Thread t2 = new Thread(() =>
{
    while (true)
    {
        myEvent += myListener;
        myEvent -= myListener;
    }
});

t1.Start();
t2.Start();

リリースモードでしばらくテストを実行しましたが、NullReferenceExceptionが発生することはありませんでした。

それで、マイク・ローゼンブラムは彼のコメントで間違っていましたか、そしてメソッドのインライン化は競合状態を引き起こすことができませんか?

実際、本当の問題は、SaifeRaiseが次のようにインライン化されるかどうかだと思います。

while (true)
{
    EventHandler handler = myEvent;
    if (handler != null)
        handler(null, EventArgs.Empty);
}

また

while (true)
{
    if (myEvent != null)
        myEvent(null, EventArgs.Empty);
}
4

3 に答える 3

7

問題はメソッドのインライン化ではなく、インライン化されているかどうかに関係なく、JITter がメモリ アクセスで興味深いことを行っていたことです。

しかし、そもそもそれが問題だとは思いません。数年前に懸念事項として提起されましたが、それは記憶モデルの読み方の誤りと見なされたと思います。変数の論理的な「読み取り」は 1 回だけであり、JITter はそれを最適化して、コピーの 1 回の読み取りとコピーの 2 回目の読み取りの間で値が変化するようにすることはできません。

編集: 明確にするために、これが問題を引き起こしている理由を正確に理解しています。基本的に、同じ変数を変更する 2 つのスレッドがあります (キャプチャされた変数を使用しているため)。コードが次のようになる可能性は十分にあります。

Thread 1                      Thread 2

                              myEvent += myListener;

if (myEvent != null) // No, it's not null here...

                              myEvent -= myListener; // Now it's null!

myEvent(null, EventArgs.Empty); // Bang!

変数は通常の静的/インスタンス フィールドではなく、キャプチャされた変数であるため、このコードでは通常よりも少しわかりにくくなっています。ただし、同じ原則が適用されます。

セーフ レイズ アプローチのポイントは、他のスレッドから変更できないローカル変数に参照を格納することです。

EventHandler handler = myEvent;
if (handler != null)
{
    handler(null, EventArgs.Empty);
}

スレッド 2 が値を変更するかどうかは問題ではありません。myEventハンドラーの値を変更できないため、NullReferenceException.

JITinlineSafeRaiseを実行する場合、このスニペットにインライン化されます。これは、インライン化されたパラメーターが効果的に新しいローカル変数になるためです。問題は、JITmyEvent.

さて、なぜこれがデバッグ モードでしか起きなかったのかというと、デバッガーが接続されていると、スレッドが互いに割り込む余地がはるかに大きくなったのではないかと思います。他の最適化が行われた可能性がありますが、破損は発生していないため、問題ありません。

于 2010-02-24T16:07:13.653 に答える
5

これはメモリ モデルの問題です。

基本的に問題は、コードに論理読み取りが 1 つしか含まれていない場合、オプティマイザーが別の読み取りを導入できるかどうかです。

驚くべきことに、答えは次のとおりです

CLR 仕様では、オプティマイザーがこれを行うことを妨げるものは何もありません。最適化によってシングルスレッドのセマンティクスが破られることはなく、メモリ アクセス パターンは volatile フィールドに対してのみ保持されることが保証されます (これは単純化であり、100% 真実ではありません)。

したがって、ローカル変数とパラメーターのどちらを使用しても、コードはスレッドセーフではありません

ただし、Microsoft .NET フレームワークでは、別のメモリ モデルが文書化されています。そのモデルでは、オプティマイザーが読み取りを導入することは許可されておらず、コードは安全です(インライン化の最適化とは無関係)。

とはいえ、[MethodImplOptions] を使用するのは奇妙なハックのように思えます。オプティマイザーが読み取りを導入するのを防ぐことは、インライン化しないことの副作用に過ぎないからです。代わりに volatile フィールドまたは Thread.VolatileRead を使用します。

于 2010-03-02T19:04:33.007 に答える
1

正しいコードでは、最適化によってそのセマンティクスが変更されることはありません。したがって、エラーがまだコードに含まれていない場合、オプティマイザによってエラーが発生することはありません。

于 2010-02-24T16:06:42.073 に答える