13

次のコードに競合状態が発生する可能性がありNullReferenceExceptionますか?

- また -

Callbacknull合体演算子がnull値をチェックした後、関数が呼び出される前に、変数がnullに設定される可能性はありますか?

class MyClass {
    public Action Callback { get; set; }
    public void DoCallback() {
        (Callback ?? new Action(() => { }))();
    }
}

編集

これは好奇心から生まれた質問です。私は通常、このようにコーディングしません。

Callback変数が古くなる心配はありません。Exceptionから投げ出されるのが心配ですDoCallback

編集#2

これが私のクラスです:

class MyClass {
    Action Callback { get; set; }
    public void DoCallbackCoalesce() {
        (Callback ?? new Action(() => { }))();
    }
    public void DoCallbackIfElse() {
        if (null != Callback) Callback();
        else new Action(() => { })();
    }
}

メソッドDoCallbackIfElseには、NullReferenceException. DoCallbackCoalesceメソッドは同じ条件を持っていますか?

そして、ここにIL出力があります:

MyClass.DoCallbackCoalesce:
IL_0000:  ldarg.0     
IL_0001:  call        UserQuery+MyClass.get_Callback
IL_0006:  dup         
IL_0007:  brtrue.s    IL_0027
IL_0009:  pop         
IL_000A:  ldsfld      UserQuery+MyClass.CS$<>9__CachedAnonymousMethodDelegate1
IL_000F:  brtrue.s    IL_0022
IL_0011:  ldnull      
IL_0012:  ldftn       UserQuery+MyClass.<DoCallbackCoalesce>b__0
IL_0018:  newobj      System.Action..ctor
IL_001D:  stsfld      UserQuery+MyClass.CS$<>9__CachedAnonymousMethodDelegate1
IL_0022:  ldsfld      UserQuery+MyClass.CS$<>9__CachedAnonymousMethodDelegate1
IL_0027:  callvirt    System.Action.Invoke
IL_002C:  ret         

MyClass.DoCallbackIfElse:
IL_0000:  ldarg.0     
IL_0001:  call        UserQuery+MyClass.get_Callback
IL_0006:  brfalse.s   IL_0014
IL_0008:  ldarg.0     
IL_0009:  call        UserQuery+MyClass.get_Callback
IL_000E:  callvirt    System.Action.Invoke
IL_0013:  ret         
IL_0014:  ldsfld      UserQuery+MyClass.CS$<>9__CachedAnonymousMethodDelegate3
IL_0019:  brtrue.s    IL_002C
IL_001B:  ldnull      
IL_001C:  ldftn       UserQuery+MyClass.<DoCallbackIfElse>b__2
IL_0022:  newobj      System.Action..ctor
IL_0027:  stsfld      UserQuery+MyClass.CS$<>9__CachedAnonymousMethodDelegate3
IL_002C:  ldsfld      UserQuery+MyClass.CS$<>9__CachedAnonymousMethodDelegate3
IL_0031:  callvirt    System.Action.Invoke
IL_0036:  ret    

演算子call UserQuery+MyClass.get_Callbackを使用すると 1 回だけ呼び出され、 を使用すると 2 回呼び出されるように見えます。私は何か間違ったことをしていますか???if...else

4

4 に答える 4

11
public void DoCallback() {
    (Callback ?? new Action(() => { }))();
}

以下と同等であることが保証されています。

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

これにより NullReferenceException が発生するかどうかは、メモリ モデルによって異なります。Microsoft .NET フレームワークのメモリ モデルは、追加の読み取りを導入しないように文書化されているため、テスト対象の値nullは呼び出される値と同じであり、コードは安全です。ただし、ECMA-335 CLI メモリ モデルはそれほど厳密ではなく、ランタイムがローカル変数を削除してCallbackフィールドに 2 回アクセスできるようにします (単純なフィールドにアクセスするフィールドまたはプロパティであると想定しています)。

Callback適切なメモリ バリアが使用されていることを確認するためにフィールドをマークする必要がvolatileあります。これにより、脆弱な ECMA-335 モデルでもコードが安全になります。

パフォーマンスが重要なコードでない場合は、ロックを使用してください (ロック内のローカル変数に Callback を読み込むだけで十分です。デリゲートを呼び出している間はロックを保持する必要はありません)。安全であり、正確な詳細は将来の .NET バージョンで変更される可能性があります (Java とは異なり、Microsoft は .NET メモリ モデルを完全には指定していません)。

于 2012-05-12T20:29:25.733 に答える
11

アップデート

編集によって古い値を取得する問題を除外すると、null 合体オプションは常に確実に機能します(正確な動作を特定できない場合でも)。ただし、代替バージョン(そうでない場合はnullそれを呼び出す)はそうではなく、NullReferenceException.

null 合体演算子は、Callback1 回だけ評価されます。デリゲートは不変です:

結合や削除などの結合操作は、既存のデリゲートを変更しません。代わりに、このような操作は、操作の結果を含む新しいデリゲート、変更されていないデリゲート、または null を返します。結合操作は、操作の結果が少なくとも 1 つのメソッドを参照しないデリゲートである場合、null を返します。結合操作は、要求された操作が効果がない場合、変更されていないデリゲートを返します。

さらに、デリゲートは参照型であるため、単純な読み取りまたは書き込みはアトミックであることが保証されています (C# 言語仕様、5.5 項)。

次のデータ型の読み取りと書き込みはアトミックです: bool、char、byte、sbyte、short、ushort、uint、int、float、および参照型。

これは、null 合体演算子が無効な値を読み取る方法がないこと、および値が 1 回だけ読み取られるため、エラーの可能性がないことを確認します。

一方、条件付きバージョンはデリゲートを 1 回読み取り、次に 2 回目の独立した読み取りの結果を呼び出します。最初の読み取りが null 以外の値を返し、2 回目の読み取りが発生する前にデリゲートが (アトミックに、しかしそれは役に立たない) で上書きされnullた場合、コンパイラは null 参照で呼び出しInvokeを終了するため、例外がスローされます。

これらはすべて、2 つのメソッドの IL に反映されています。

元の回答

反対の明示的な文書がない場合は、そうです。より単純な場合にもあるように、ここには競合状態があります。

public int x = 1;

int y = x == 1 ? 1 : 0;

原則は同じです。最初に条件が評価され、次に式の結果が生成されます (後で使用されます)。状態が変化するようなことが起こってからでは手遅れです。

于 2012-05-12T17:17:01.560 に答える
3

このコードには競合状態は見られません。潜在的な問題がいくつかあります。

  • Callback += someMethod;アトミックではありません。単純な割り当てです。
  • DoCallback古い値を呼び出すことができますが、一貫性があります。
  • 古い値の問題は、コールバックの全期間にわたってロックを維持することによってのみ回避できます。しかし、それはデッドロックを招く非常に危険なパターンです。

より明確な書き方DoCallbackは次のようになります。

public void DoCallback()
{
   var callback = Callback;//Copying to local variable is necessary
   if(callback != null)
     callback();
}

isの場合、no-op デリゲートを作成して呼び出さないため、元のコードよりも少し高速Callbackですnull


また、プロパティをイベントに置き換えて、アトミック+=およびを取得することもでき-=ます。

 public event Action Callback;

+=プロパティを呼び出すと、 Callback = Callback + someMethod. Callbackこれは、読み取りと書き込みの間で変更される可能性があるため、アトミックではありません。

+=フィールドのようなイベントを呼び出すと、イベントのメソッドが呼び出されますSubscribe。イベント サブスクリプションは、フィールドのようなイベントに対してアトミックであることが保証されています。実際には、Interlockedこれを行うために何らかの手法を使用します。


null 合体演算子の使用は、??ここではあまり重要ではなく、本質的にスレッド セーフでもありません。重要なのは、一度だけ読むことですCallback??スレッドセーフではない類似のパターンが他にもあります。

于 2012-05-12T17:22:47.997 に答える
0

1行だから無事だったのかな?通常はそうではありません。共有メモリにアクセスする前に、lock ステートメントを使用する必要があります。

于 2012-05-12T17:17:23.237 に答える