オブジェクトが存在しない場合のみ生成したい
ここ数日で実装したBTKInjectorにおいて、インスタンス変数をnilで初期化し、初回アクセス時にオブジェクトを割り当てる必要がでてきました。
すごく単純に書くとこんな感じ。
- (id)get
{
if(_obj == nil){
_obj = [self getImpl];
}
return _obj;
}
マルチスレッド対応
ロックなしだとマルチスレッドで適切に動作しないので、 @synchronizedを使って同期処理を追加します。
- (id)get
{
@synchronized(self){
if(_obj == nil){
_obj = [self getImpl];
}
return _obj;
}
}
完。。。
ではないですね、今回はDIのProvider実装なので、毎回ロックを取得するのは好ましくありません。
Double-Checked Locking
そこで有名なDouble-Checked Locking。同期処理をする前に一度変数をチェックすることで、初回以外はロックが必要なくなります。
- (id)get
{
if(_obj == nil){
@synchronized(self){
if(_obj == nil){
id obj = [self getImpl];
_obj = obj;
}
}
}
return _obj;
}
これでOK。。。
でもないんですよね。そこまで含めて有名だと思います。プログラムは、書かれた順番で実行されるとは限りません。
- コンパイラによる最適化
- CPUによる最適化
という二つの要素により、_objにオブジェクトが設定されているにも関わらず、そのオブジェクトの初期化が完了していない可能性があります。
対策として下記の二つが必要です。
- volatile宣言で、コンパイラによるメモリアクセス最適化を抑制
- OSMemoryBarrierで、CPUがメモリを逐次処理するように強制
というわけで、正しくはこちら。
@implementation BTKInjectorProviderBase{
id volatile _obj;
}
- (id)get
{
if(_obj == nil){
@synchronized(self){
if(_obj == nil){
id obj = [self getImpl];
OSMemoryBarrier();
_obj = obj;
}
}
}
OSMemoryBarrier();
return _obj;
}
OSMemoryBarrierのコストは発生しますが、synchronizeするのに比べればはるかに小さいと思います。検証はしてないですが。
dispatch_onceは使えないのか?
Singletonを作る場合であれば、GCDのdispatch_onceを使うことができます。しかし、dispatch_once_tはかならずglobalもしくはstaticなスコープに置く必要があるため、今回のケースでは使用できません。
The predicate must point to a variable stored in global or static scope. The result of using a predicate with automatic or dynamic storage (including Objective-C instance variables) is undefined.