OITA: Oika's Information Technological Activities

@oika 情報技術的活動日誌。

C# イベントを一時変数に入れてスレッドセーフにnullチェックするあれ

C#で、自前でイベントを発火させる際、
ハンドラが1つも登録されていない状態でInvokeしようとすると
NullReferenceExceptionになっちゃうんで、
Nullでないことを確認してから呼び出す必要があります。

public event PropertyChangedEventHandler PropertyChanged;  

protected void RaisePropertyChanged(string propertyName)  
{  
    //nullでないことを確認してから  
    if (PropertyChanged != null)  
    {  
        //発火  
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));  
    }  
}  

シングルスレッドで動くことがわかってる場合は
上のような書き方でも問題ないんだけども、
マルチスレッドで動く場合はちょいとまずい。

nullチェックをしてから発火させるまでの間に
別スレッドからPropertyChangedのハンドラを削除される可能性があるからだ。
実際、ハンドラの出し入れとイベント発火を別スレッドでループさせるような
検証コードを書いて試せば、簡単にNullReferenceを発生させられるのがわかる。

なので、下のように書く人が多いでしょう。

protected void RaisePropertyChanged(string propertyName)  
{  
    //一時変数に代入  
    var temp = PropertyChanged;  

    if (temp != null)  
    {  
        temp(this, new PropertyChangedEventArgs(propertyName));  
    }  
}  

ところがところが。
プログラミング.NET Framework 第4版』によると、
このような書き方も若干の問題を含んでいるらしい。

デリゲートが不変なので、このテクニックが理論上は上手くいくことを覚えておいてください。ただし、驚くほど多くの開発者が、コンパイラによる最適化によってローカル変数tempが完全に削除される可能性があることを知らないのです。最適化が行われると、このバージョンのコードは1番目のバージョンと同じになるので、依然としてNullReferenceExceptionが発生する可能性があるのです。

コンパイラの最適化によって最初の書き方と同じにされる可能性がありますよと。
じゃあどうすればいいのってーと、下のように書きましょうと。

protected void RaisePropertyChanged(string propertyName)  
{  
    var temp = Volatile.Read(ref PropertyChanged);  

    if (temp != null)  
    {  
        temp(this, new PropertyChangedEventArgs(propertyName));  
    }  
}  

Volatile.Readでその時点でのイベントの中身を強制的に読み取って
参照を確実に一時変数にコピーさせればOKですということらしい。

ただし、こうも書いてある。

3番目のバージョンのコードは最善で、技術的に正しいのですが、実際には2番目のバージョンを使用することができます。なぜなら、JITコンパイラは2番目のバージョンのパターンを識別でき、ローカルのtemp変数を最適化として削除してはならないことを認識しているからです。具体的には、MicrosoftのJITコンパイラはすべて、ヒープメモリに対する新しい読み取りを追加しないことで不変性を尊重しており、したがってローカル変数への参照をキャッシュすることでヒープ上の参照に一度だけアクセスすることを保証しています。このことは文書化されておらず、したがって理論的には変更される可能性があり、これこそが3番目のバージョンを使用すべき理由です。

現状はちゃんと動くけど、将来的にはわかりませんよということだ。
とはいえ、実際には、あまりに多くのアプリが動かなくなっちゃうから
こんな変更がJITコンパイラに取り入れられることはないでしょうとも書いてあった。

さて、ところでC# 6.0からは Null条件演算子 があるので、
実はこれまでのようなめんどっちい書き方をする必要がないのである。

protected void RaisePropertyChanged(string propertyName)  
{  
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));  
}  

これだけ!1行!
え、これスレッドセーフなの?って思うところだが、
ちゃんとMSDNに書いてあった。

コンパイラが PropertyChanged を評価するためのコードを一度しか生成せず、一時変数に結果が保持されるため、新しい方法はスレッド セーフです。

やるやん。

ちなみに、もいっこnull対策としてよく使われるのは
以下のように、初期化時に空のデリゲートを登録してしまう戦法。

public event PropertyChangedEventHandler PropertyChanged = delegate { };  

こうしておけば、少なくともクラス外からは
このイベントをNullにすることができない。
習慣化できるならこれもありっすね。