26

私はこの質問をして、この興味深い(そして少し戸惑う)答えを得ました.

ダニエルは、 ECMA-335 CLINullReferenceException仕様により、コンパイラが次のDoCallbackメソッドからa をスローするコードを生成できる可能性があると回答で述べています (私が間違って読んでいる場合を除きます) 。

class MyClass {
    private Action _Callback;
    public Action Callback { 
        get { return _Callback; }
        set { _Callback = value; }
    }
    public void DoCallback() {
        Action local;
        local = Callback;
        if (local == null)
            local = new Action(() => { });
        local();
    }
}

NullReferenceException彼は、 aがスローされないことを保証するために、volatileキーワードを で使用する_Callbacklock、行の周りで a を使用する必要があると述べていますlocal = Callback;

誰かがそれを裏付けることができますか? もしそれが本当なら、この問題に関してMonoコンパイラと.NETコンパイラの動作に違いはありますか?

編集ここに標準
へのリンクがあります。

更新
これは仕様の適切な部分だと思います(12.6.4):

CLI の適合実装は、単一の実行スレッド内で、スレッドによって生成された副作用と例外が CIL によって指定された順序で表示されることを保証する任意のテクノロジを使用して、プログラムを自由に実行できます。この目的のために、揮発性操作 (揮発性読み取りを含む) のみが目に見える副作用を構成します。(揮発性操作のみが目に見える副作用を構成しますが、揮発性操作は不揮発性参照の可視性にも影響することに注意してください。) 揮発性操作は §12.6.7 で指定されています。別のスレッドによってスレッドに注入された例外に関連する順序の保証はありません (このような例外は「非同期例外」と呼ばれることがあります (例: System.Threading.ThreadAbortException))。

[理論的根拠: 最適化コンパイラは、この並べ替えが観察可能なプログラムの動作を変更しない範囲で、副作用と同期例外を自由に並べ替えることができます。終了理由]

[注: CLI の実装は、最適化コンパイラを使用することが許可されています。たとえば、CIL をネイティブ マシン コードに変換するために、コンパイラが (実行の各単一スレッド内で) 同じ順序の副作用と同期例外を維持する場合に限ります。

だから...私は、このステートメントにより、コンパイラがCallbackプロパティ(単純なフィールドにアクセスする)とlocal変数を最適化して、単一の実行スレッドで同じ動作をする次のものを生成できるかどうかに興味があります:

if (_Callback != null) _Callback();
else new Action(() => { })();

キーワードの 12.6.7 セクションはvolatile、最適化を回避したいプログラマーに解決策を提供しているようです。

揮発性読み取りには「取得セマンティクス」があります。つまり、読み取りは、CIL 命令シーケンスの読み取り命令の後に発生するメモリへの参照の前に発生することが保証されます。揮発性書き込みには「解放セマンティクス」があります。つまり、書き込みは、CIL 命令シーケンスの書き込み命令の前のメモリ参照の後に発生することが保証されます。CLI の適合実装は、揮発性操作のこのセマンティクスを保証するものとします。これにより、すべてのスレッドが、他のスレッドによって実行された揮発性書き込みを、実行された順序で監視できるようになります。しかし、適合する実装は、実行のすべてのスレッドから見た揮発性書き込みの単一の全体的な順序を提供する必要はありません。

4

2 に答える 2

13

C#によるCLR (pp. 264–265) では、Jeffrey Richter がこの特定の問題について議論し、ローカル変数がスワップ アウトされる可能性があることを認めています。

[T]このコードはコンパイラによって最適化され、ローカル […] 変数が完全に削除される可能性があります。これが発生した場合、このバージョンのコードは [イベント/コールバックを直接 2 回参照するバージョン] と同じであるため、NullReferenceException引き続き可能です。

Richter は、Interlocked.CompareExchange<T>この問題を決定的に解決するために を使用することを提案しています。

public void DoCallback() 
{
    Action local = Interlocked.CompareExchange(ref _Callback, null, null);
    if (local != null)
        local();
}

ただし、Richter は、Microsoft の Just-In-Time (JIT) コンパイラがローカル変数を最適化しないことを認めています。これは理論的には変更される可能性がありますが、結果として多くのアプリケーションが機能しなくなるため、ほぼ確実に変更されることはありません。

この質問は、「<a href="https://stackoverflow.com/questions/7664046/allowed-c-sharp-compiler-optimization-on-local-variables-and-refetching-value -fr">ローカル変数に対する C# コンパイラの最適化と、メモリからの値の再フェッチを許可しました。xanatoxによる回答と「<a href="http://msdn.microsoft.com/en-us/magazine/cc163715.aspx#S6" rel="nofollow noreferrer">Low の影響を理解する」を必ずお読みください。-マルチスレッド アプリでのロック テクニック」という記事が引用されています。特に Mono について質問されたので、参照されている「<a href="http://www.mail-archive.com/mono-devel-list@lists.ximian.com/msg17446.html" rel="」に注意してください。 nofollow noreferrer">[Mono-dev] メモリ モデル?」メーリング リスト メッセージ:

現在、実行中のアーキテクチャに裏打ちされた ecma に近い緩やかなセマンティクスを提供しています。

于 2012-05-15T21:16:50.210 に答える
3

このコードはnull 参照例外をスローしません。これはスレッドセーフです:

public void DoCallback() {
    Action local;
    local = Callback;
    if (local == null)
        local = new Action(() => { });
    local();
}

これがスレッドセーフで、コールバックで NullReferenceException をスローできない理由は、null チェック/呼び出しを行う前にローカル変数にコピーしているためです。null チェック後に元の Callback が null に設定されていたとしても、ローカル変数は引き続き有効です。

ただし、次の話は別です。

public void DoCallbackIfElse() {
    if (null != Callback) Callback();
    else new Action(() => { })();
}

これでは、パブリック変数を調べています。Callback は、後でif (null != Callback)例外をスローするように null に変更できます。Callback();

于 2012-05-14T19:25:15.013 に答える