2017年12月15日
川俣晶の縁側ソフトウェア技術雑記 total 2457 count

LazyInitializer.EnsureInitializedメソッドで初期化しているのにコンストラクタが2回走る

Written By: 川俣 晶連絡先

問題 §

 マルチスレッド環境で、EnsureInitialized<T>(T) メソッドを使用して自作クラス型の変数を初期化しているが、希に自作クラスのコンストラクタが2回走ってしまうケースがある。

 LazyInitializer.EnsureInitializedメソッドはスレッドセーフであるというネットの情報は見ている。

原因 §

 EnsureInitialized<T>(T) メソッドは変数の書き換えに関してはスレッドセーフであるが、コンストラクタの実行に関してはスレッドセーフではない。

 もし、変数が未初期化であることを発見すると、新しいオブジェクトの作成を実行してしまう構造なので、【変数の値が常に健全である】ということは保証されるが、【コンストラクタが1回だけ実行される】ということは保証されない。

 (EnsureInitialized<T>(T) のソースコードを確認した結果、変数の書き換えはCompareExchangeで行うので、変数の変化はnullから有効な値への変化1回きりである。それ以後は書き込もうとしても書き込めない。つまり、2回目以降に実行されたコンストラクタが生成した値は破棄される)

解決 §

 もし、マルチスレッド環境下でコンストラクタが1回だけ実行されることを保証したいなら、EnsureInitialized<T>(T)ではなく、EnsureInitialized<T>(T, Boolean, Object) を使用する。2回コンストラクタを実行させないためのポイントは、以下の2つ。

  • 第2引数に対して全てのスレッドの呼び出しで【同じ】bool型変数を格納すること
  • 第3引数に対し、全てのスレッドの呼び出しで【同じ】同期オブジェクトを指定すること

 スレッド間で同じ変数を指定することは非常に重要。第2引数で指定する変数は必ずfalseで初期化しておく。第3引数の同期オブジェクトは変数の値がnullの場合は自動的に作成されるので初期化は重要ではない。

感想 §

「かなり時間を食ってしまったが、だいたい明らかにできた。ドキュメントがあまりに不十分で細部の動作をチェックするためにソースまで見る羽目になったのは非常によろしくないがな」

「ひぇ~。また.NET Frameworkのソース見たのかよ」

「おかげで、ほぼ確実と言えるレベルで解決策を説明できたけどな」