はじめに
遅延初期化とは、必要になったときに必要になった分だけ処理して、効率的に処理を行うパターンを言います。
C#に限らない話ですが、ファイルやDB等の外部リソースを扱うアプリケーションにおいて、PocoなりEntityなりでデータを保持するようなコードを書くことが多くあります。このデータ取得処理を、データが必要になってから行うようにすることで、全体の計算量を削減することができるようになります。
外部リソースを使用するとき、基本的にコストが発生します(時間とか)。
モデル内にデータを保持できるプロパティを作成しておき、すでに取得済みなら内部で保持しているものを返し、未取得なら外部リソースを触るコードを書くことで、無駄なアクセスが抑制され、効率的に処理することができるようになることは自明だと思います。
その時のデータの取り扱い方についてメモとして書きます。
インスタンス変数がnullかどうかで分岐するパターン
一番単純なやつ。こんなふうに書きます。
public class Model {
priate string[] _datas = null;
public string[] Datas {
get { return _datas ?? (_datas = GetDatas()); }
}
priate string[] GetDatas() {
var adpater = new DataAdapter(); // 外部リソースにアクセスする何か
return adapter.GetData();
}
}
実装自体はすごくシンプルです。よくある系。
ただし、こいつには1点大きな弱点があります。
- スレッドセーフ、なにそれおいしいの?
マルチスレッドで同時アクセスが行われないことが明確になっている場合、
例えば、Webアプリで1リクエスト中一回しか呼ばれないケース、では特に問題ありません。
しかし、同時に複数のスレッドから同時にアクセスされるケース(Parallelsとか)を使う場合、Dataプロパティ中での取得済みチェックがうまく働かず、複数回GetDatas()が叩かれることになります。
lockステートメントや、ReaderWriterLockなどを駆使して解決することもできますが、これを意識せずに書ける構文が、C#に存在します。
System.Lazyを使うパターン
Lazyというクラスが存在しています。
その名の通り、遅延実行をサポートしてくれるものです、、、 .net4.0からの登場。
最初にLazy.Valueが呼び出された時だけ、Lazyのコンストラクタに渡されたFuncを実行する仕組みになっており、二回目のValue呼び出し時は、1回めに取得した結果を返す性質を持っています。
実際に書くとこんな感じ。
public class Model {
priate Lazy<string[]> _datas = null;
public Model() {
_data = new Lazy<string[]>(GetDatas); // ポイント
}
public string[] Datas { get { return _datas.Value; } }
priate string[] GetDatas() {
var adpater = new DataAdapter(); // 外部リソースにアクセスする何か
return adapter.GetData();
}
}
Lazyを使って嬉しいところは、排他制御が内部で行われており、スレッドセーフなところです。
自力でスレッドセーフな排他処理を実装するより、楽に実装できます。
構文の複雑さは好みが別れるかも。
例のようにメソッド呼び出すだけならいいんですが、複雑なFuncを書く必要が出た時に、大変読みづらくなります。
もう一つ。
やはり排他制御から離れられない仕組みになっているので、実行速度はあまり良くはありません。
一番良いのは、排他制御から開放されつつ、スレッドセーフを享受すること。
しかし、そんな書き方は存在するんでしょうか?
それは、次に紹介するThreadLocalが解決してくれます。
System.Threading.ThreadLocalを使うパターン
ThreadLocalは、処理するスレッド毎にインスタンスを生成できる仕組みを持つものです。
こちらも.net4.0が初出ですが、あまり知られていない気がします。(自分も人に教えてもらって知りました)
TLS(スレッドローカルストレージ)といった方がわかりやすいかも。
スレッド毎にインスタンスを持つ、ということは、「他のスレッドから触られない」すなわち「スレッドセーフである必要が薄い」ということが言えます。
実際の実装を見ても、コンストラクタと、Dispose時に、InterlockedやConcurrencyといった排他制御を行っているものの、実際にデータアクセスするときには排他制御が行われていません。
実際の使い方は、Lazyと同じです。
public class Model {
priate ThreadLocal<string[]> _datas = null;
public Model() {
_data = new ThreadLocal<string[]>(GetDatas); // ポイント
}
public string[] Datas { get { return _datas.Value; } }
priate string[] GetDatas() {
var adpater = new DataAdapter(); // 外部リソースにアクセスする何か
return adapter.GetData();
}
}
ただ、スレッド毎にインスタンスを生成してしまうため、かなり富豪的な処理だと思います。
大きなデータな可能性がある外部リソースの保持には使いにくいかもしれません。排他制御のコストが許容できないケースや、Adapterをスレッド数分増やして並列処理ぶん回す時に使うことになるかと思います( ・`ω・´)
終わりに
遅延初期化は、比較的簡単に実装できる上、効果も高く、コスパが優れている方法だと思います。
幾つかやり方を書きましたが、やはりどのやり方も得手不得手があるため、ひとつの方法をなんとなく使うより、用途を絞って使うことが賢いやりかただと思います( ・`ω・´)
非同期処理を行う可能性のあるところはLazyで。非同期で処理しないところはインスタンス変数のNull判定で。バッチ等で並列処理ぶん回す時にThreadLoaclと、自分はこんな風に使い分けしております。
この投稿が何かの参考になれば幸いです。