今回は、System.Lazyクラスについてちょこっとメモメモ。
Lazyクラスは.NET Framework 4.0から追加された型です。
名前の通り、遅延初期化をサポートするためのクラスです。
スレッド処理でよくある共通の問題として、如何にして共有フィールドの遅延初期化を
スレッドセーフに行うかという問題があります。
マルチスレッドの場合、単純なオブジェクトの構築の場合でも気をつける必要があります。
たとえば、HeavyObjectというクラスがあったとして、このクラスの初期化には
すこし時間がかかるとします。また、利用される頻度はそんなに多くないけど必要であるとします。
その場合、以下のように
class Hoge { public readonly HeavyObject Heavy = new HeavyObject(); }
と、宣言していると毎回Hogeの初期化時にHeavyObjectも構築されるので
ちょっと無駄な場合があります。
で、次に遅延初期化を考えます。必要とされる最初の段階まで
インスタンスの構築を遅らせます。
class Hoge { HeavyObject _heavy; public HeavyObject Heavy { get { if (_heavy == null) { _heavy = new HeavyObject(); } return _heavy; } } }
シングルスレッドな場合はこれで大丈夫です。
でも、これをマルチスレッドな環境で実行した場合は大丈夫でしょうか?つまりスレッドセーフかどうか?
複数のスレッドが同時にプロパティに入ってきたら?同時にif文の評価が行われたら?
状況によっては、同時にインスタンスが生成され、上書きされます。
結論として、上記の処理はスレッドセーフではありません。
通常の解決方法としては、ロックを行うようにします。
(実際には、double-checked lockingやrace-to-initializeパターンなどいろいろな方法があります。)
class Hoge { HeavyObject _heavy; readonly object _lock = new object(); public HeavyObject Heavy { get { lock (_lock) { if (_heavy == null) { _heavy = new HeavyObject(); } return _heavy; } } } }
double-checked lockingの場合
class Hoge { volatile HeavyObject _heavy; readonly object _lock = new object(); public HeavyObject Heavy { get { if (_heavy == null) { lock (_lock) { if (_heavy == null) { _heavy = new HeavyObject(); } } } return _heavy; } } }
race-to-initializeパターンの場合
class Hoge { volatile HeavyObject _heavy; public HeavyObject Heavy { get { if (_heavy == null) { var heavy = new HeavyObject(); Interlocked.CompareExchange(ref _heavy, heavy, null); } return _heavy; } } }
前置きが長くなりましたが、Lazyクラスは上記のような遅延初期化を
行うためのクラスです。利用方法は、以下のようになります。
var lazy1 = new Lazy<HeavyObject>(() => new HeavyObject(), true);
または
var lazy1 = new Lazy<HeavyObject>(() => new HeavyObject(), LazyThreadSafetyMode.ExecutionAndPublication);
です。
LazyThreadSafetyModeにてExecutionAndPublicationを指定するとdouble-checked lockingパターン、
PublicationOnlyを指定すると、race-to-initializeパターンに対応した処理が実行されます。
先ほどのサンプルの場合は、以下のように書き換えられます。
class Hoge { Lazy<HeavyObject> _heavy = new Lazy<HeavyObject>(() => new HeavyObject(), LazyThreadSafetyMode.ExecutionAndPublication); public HeavyObject Heavy { get { return _heavy.Value; } } }
以下サンプルです。
#region LazySamples-01 /// <summary> /// Lazy<T>, LazyInitializerクラスのサンプルです。 /// </summary> public class LazySamples01 : IExecutable { public void Execute() { // // Lazy<T>クラスは、遅延初期化 (Lazy Initialize)機能を付与するクラスである。 // // 利用する際は、LazyクラスのコンストラクタにFunc<T>を指定することにより // 初期化処理を指定する。(たとえば、コストのかかるオブジェクトの構築などをFuncデリゲート内にて処理など) // // また、コンストラクタにはFunc<T>の他にも、第二引数としてスレッドセーフモードを指定出来る。 // (System.Threading.LazyThreadSafetyMode) // // スレッドセーフモードは、Lazyクラスが遅延初期化処理を行う際にどのレベルのスレッドセーフ処理を適用するかを指定するもの。 // スレッドセーフモードの指定は、LazyクラスのコンストラクタにてLazyThreadSafetyModeかboolで指定する。 // ・None: スレッドセーフ無し。速度が必要な場合、または、呼び元にてスレッドセーフが保証出来る場合に利用 // ・PublicationOnly: 複数のスレッドが同時に値の初期化を行う事を許可するが、最初に初期化に成功したスレッドが // Lazyインスタンスの値を設定するモード。(race-to initialize) // ・ExecutionAndPublication: 完全スレッドセーフモード。一つのスレッドのみが初期化を行えるモード。 // (double-checked locking) // // Lazyクラスのコンストラクタにて、スレッドセーフモードをbool型で指定する場合、以下のLazyThreadSafetyModeの値が指定された事と同じになる。 // ・true : LazyThreadSafetyMode.ExecutionAndPublicationと同じ。 // ・false: LazyThreadSafetyMode.Noneと同じ。 // // Lazyクラスは、例外のキャッシュ機能を持っている。これは、Lazy.Valueを呼び出した際にコンストラクタで指定した // 初期化処理内で例外が発生した事を検知する際に利用する。Lazyクラスのコンストラクタにて、既定コンストラクタを使用するタイプの // 設定を行っている場合、例外のキャッシュは有効にならない。 // // また、LazyThreadSafetyMode.PublicationOnlyを指定した場合も、例外のキャッシュは有効とならない。 // // 排他モードで初期化処理を実行 var lazy1 = new Lazy<HeavyObject>(() => new HeavyObject(TimeSpan.FromMilliseconds(100)), LazyThreadSafetyMode.ExecutionAndPublication); // 尚、上は以下のように第二引数をtrueで指定した場合と同じ事。 // var lazy1 = new Lazy(() => new HeavyObject(TimeSpan.FromSeconds(1)), true); // 値が初期化済みであるかどうかは、IsValueCreatedで確認出来る。 Console.WriteLine("値構築済み? == {0}", lazy1.IsValueCreated); // // 複数のスレッドから同時に初期化を試みてみる。 (ExecutionAndPublication) // Parallel.Invoke ( () => { Console.WriteLine("[lambda1] 初期化処理実行 start."); if (lazy1.IsValueCreated) { Console.WriteLine("[lambda1] 既に値が作成されている。(IsValueCreated=true)"); } else { Console.WriteLine("[lambda1] ThreadId={0}", Thread.CurrentThread.ManagedThreadId); var obj = lazy1.Value; } Console.WriteLine("[lambda1] 初期化処理実行 end."); }, () => { Console.WriteLine("[lambda2] 初期化処理実行 start."); if (lazy1.IsValueCreated) { Console.WriteLine("[lambda2] 既に値が作成されている。(IsValueCreated=true)"); } else { Console.WriteLine("[lambda2] ThreadId={0}", Thread.CurrentThread.ManagedThreadId); var obj = lazy1.Value; } Console.WriteLine("[lambda2] 初期化処理実行 end."); } ); Console.WriteLine("=========================================="); // // 複数のスレッドにて同時に初期化処理の実行を許可するが、最初に初期化した値が設定されるモード。 // (PublicationOnly) // var lazy2 = new Lazy<HeavyObject>(() => new HeavyObject(TimeSpan.FromMilliseconds(100)), LazyThreadSafetyMode.PublicationOnly); Parallel.Invoke ( () => { Console.WriteLine("[lambda1] 初期化処理実行 start."); if (lazy2.IsValueCreated) { Console.WriteLine("[lambda1] 既に値が作成されている。(IsValueCreated=true)"); } else { Console.WriteLine("[lambda1] ThreadId={0}", Thread.CurrentThread.ManagedThreadId); var obj = lazy2.Value; } Console.WriteLine("[lambda1] 初期化処理実行 end."); }, () => { Console.WriteLine("[lambda2] 初期化処理実行 start."); if (lazy2.IsValueCreated) { Console.WriteLine("[lambda2] 既に値が作成されている。(IsValueCreated=true)"); } else { Console.WriteLine("[lambda2] ThreadId={0}", Thread.CurrentThread.ManagedThreadId); var obj = lazy2.Value; } Console.WriteLine("[lambda2] 初期化処理実行 end."); } ); Console.WriteLine("値構築済み? == {0}", lazy1.IsValueCreated); Console.WriteLine("値構築済み? == {0}", lazy2.IsValueCreated); Console.WriteLine("lazy1のスレッドID: {0}", lazy1.Value.CreatedThreadId); Console.WriteLine("lazy2のスレッドID: {0}", lazy2.Value.CreatedThreadId); } class HeavyObject { int _threadId; public HeavyObject(TimeSpan waitSpan) { Console.WriteLine(">>>>>> HeavyObjectのコンストラクタ start. [{0}]", Thread.CurrentThread.ManagedThreadId); Initialize(waitSpan); Console.WriteLine(">>>>>> HeavyObjectのコンストラクタ end. [{0}]", Thread.CurrentThread.ManagedThreadId); } void Initialize(TimeSpan waitSpan) { Thread.Sleep(waitSpan); _threadId = Thread.CurrentThread.ManagedThreadId; } public int CreatedThreadId { get { return _threadId; } } } } #endregion
以下、実行結果です。
値構築済み? == False [lambda2] 初期化処理実行 start. [lambda2] ThreadId=4 >>>>>> HeavyObjectのコンストラクタ start. [4] [lambda1] 初期化処理実行 start. [lambda1] ThreadId=3 >>>>>> HeavyObjectのコンストラクタ end. [4] [lambda2] 初期化処理実行 end. [lambda1] 初期化処理実行 end. ========================================== [lambda1] 初期化処理実行 start. [lambda1] ThreadId=3 >>>>>> HeavyObjectのコンストラクタ start. [3] [lambda2] 初期化処理実行 start. [lambda2] ThreadId=4 >>>>>> HeavyObjectのコンストラクタ start. [4] >>>>>> HeavyObjectのコンストラクタ end. [3] [lambda1] 初期化処理実行 end. >>>>>> HeavyObjectのコンストラクタ end. [4] [lambda2] 初期化処理実行 end. 値構築済み? == True 値構築済み? == True lazy1のスレッドID: 4 lazy2のスレッドID: 3
一つ目の実行では、LazyThreadSafetyMode.ExecutionAndPublicationを指定しているので
排他モードで初期化が行われており、二つ目の実行ではPublicationOnlyを指定しているので
いったんどちらのスレッドも初期化に入ってきているのですが、最初に初期化に成功した
スレッドの値が適用されている事がわかります。
以下、参考資料です。
- Lazy
クラス - LazyThreadSafetyMode 列挙体
================================
過去の記事については、以下のページからご参照下さい。
- いろいろ備忘録日記まとめ
サンプルコードは、以下の場所で公開しています。
- いろいろ備忘録日記サンプルソース置き場