@property の atomic キーワードについて : Objective-C プログラミング
PROGRAM
@property の atomic キーワードについて
Objective-C クラスのプロパティには、複数のスレッドからプロパティを正しく読み書きできるようにするための "atomic"というキーワードが用意されています。
@property (atomic,readwrite) CGPoint location;
たとえば @interface 内でこのような定義がされていたとします。
このように "atomic" を指定することで、このプロパティは「複数のスレッドから同時にアクセスしても大丈夫」、つまり「スレッドセーフ」であるということを明示できます。
この "atomic" というキーワードは、このプロパティの値が同時にアクセスされても壊れない、という意味になります。
つまり上記の例では、CGPoint というデータ型は 2 つの数値を持つ構造体です。これを "atomic" では、あるスレッドで 1 つ目の数値を書き換えている最中に、他のスレッドに書き途中の値を取得されないように保護するという働きを意味します。
ただし、本当にスレッドセーフとして振る舞うためには、そうなるように実装してあげる必要があります。
"atomic" で保護しなければいけないもの
プリミティブ型と複合型
複数のスレッドから同時にアクセスするプロパティであれば、基本的には全ての値を "atomic" で保護しなければいけません。
冒頭の例のような複数の値を一度に扱う「構造体」の場合であれば、"書き込みの途中" という状態が存在することが感覚的に判りやすいと思います。
ほかにも「単なる 1 つの数値」であっても、SInt64 などのように CPU レベルでは 2 度に分けて書き込むような場合もあるので、この場合にも "書き込み途中" という状態が発生して、壊れたデータを取得できてしまう場合があります。
この場合にも、"atomic" で保護することが大切になります。
Objective-C クラスインスタンスを保護する場合
また、Objective-C クラスのインスタンスの場合には、少し違った気遣いが必要になってきます。
プロパティの読み書きのレベルでは、クラスインスタンスはポインタでやり取りされます。
ポインタはクラスインスタンスが保存されている場所を示す数値ですから、もちろんこれもこれまで通り、同時アクセスしたときに壊れないように保護することは大切です。
ただ、クラスインスタンスの場合は、そのインスタンスがまだ必要かどうかを "リファレンスカウンタ" という仕組みを使って管理しています。
クラスインスタンスのポインタを取得して、それを使うために retain して、使い終わったら release します。そして retain した全員が release し終えたときに、そのクラスインスタンスは dealloc を呼び出して消滅します。
これを複数のスレッドで同時に実行したとき、ポインタを取得してから retain するまでの間に隙間ができます。
この隙間に別のスレッドで release を実行されてしまうと、元のスレッドでいざ retain 使用としたときに EXC_BAD_ACCESS (deallocated instance) で強制終了してしまいます。
"atomic" では、ここも保護する必要があります。
具体的には、クラスインスタンスのポインタを取得する際には、ポインタを retain & autorelease して返すという方法をとるようです。
この流れを他のスレッドに割り込まれないように保護すれば、確実に、クラスインスタンスを取得しようとしたスレッドの autorelease プールにクラスインスタンスを入れることが可能になります。
これで、クラスインスタンスを取得した直後にそれが別のスレッドで release されても、安全にそれを使い続けられます。
なお、クラスインスタンスが扱うデータ全体の整合性は、プロパティの atomicity では保護できないので注意が必要です。
クラスインスタンスが持つ機能を、複数のスレッドから同時に利用したときのスレッドセーフを実現するには、プロパティの atomicity ではなく、@synchronized や セマフォ、ミューテックス、NSLock、NSRecursiveLock といった排他制御を使う必要があります。
"atomic" の機能を @synthesize を使って実装するには
それでは @property で指定した atomic キーワードに期待される動作を実装する方法について見て行きます。
いちばん簡単な方法は、Objective-C に用意されている @synthesize を使って実装する方法です。
@synthesize location = _location;
プロパティを実装する方法としてお馴染みのこの方法ですが、"atomic" を指定したプロパティをこれで実装すると、自動的にスレッドセーフにするための仕組みが実装されます。
Xcode 4.4 ではこの @synthesize を省略できるようになったので、かなり楽に実装できますね。
atomic 制御が実装される場合とされない場合
なお、この "atomic" キーワードは既定値なので、これを書いていないプロパティであっても @synthesize で実装すれば、スレッドセーフのためのコードが実装されます。
そういった制御が不要な場合は "nonatomic" キーワードを指定することで @synthesize がそのようなコードを作らなくなるため、プロパティの実行速度が向上します。
プリミティブ型と複合型の場合
@synthesize を使ったときの実際の "atomic" 制御のされ方ですが、プリミティブ型や複合型の場合は、次のようになるようです。
たとえば @property (atomic,readwrite) DataType value というプロパティの場合、次のような実装が自動で生成されます。
セッターの実装(プリミティブ型と複合型)
(void)setValue:(DataType)value
{
[_internal lock];
_value = value;
[_internal unlock];
}
ゲッターの実装(プリミティブ型と複合型)
(DataType)value
{
DataType result;
[_internal lock];
result = _value;
[_internal unlock];
return result;
}
だいたいこのような実装になるようです。
読み書きをロックでブロックするので、どちらかが完全に完了するまで、もう一方を実行することができなくなります。
ちなみにここの "_internal lock" や "_internal unlock" というところでは、オブジェクトレベルロックというものが使われるらしいです。
この "_internal" ロックを、プログラマが他の場所で自由に使うことはできない様子でした。
Objective-C クラスインスタンスの場合(strong または retain 指定)
Objective-C クラスインスタンスの場合は、リファレンスカウンタを考慮した実装になるため、プリミティブ型や複合型のときよりも複雑になります。
たとえば @property (atomic,readwrite,strong) ClassType* value というプロパティを @synthesize を使って自動実装した場合は、Non-ARC なコードで書くと、次のような実装が自動で生成されます。
セッターの実装(Objective-C クラスインスタンス) [strong, retain] [no-arc]
(void)setValue:(ClassType*)value
{
[_internal lock];
if (_value != value)
{
value = [value retain];
[_value release];
_value = value;
}
[_internal unlock];
}
ゲッターの実装(Objective-C クラスインスタンス) [strong, retain] [no-arc]
(ClassType*)value
{
ClassType* result;
[_internal lock];
result = [[_value retain] autorelease];
[_internal unlock];
return result;
}
プリミティブ型や複合型のときと比べて、ずいぶん様子が変わってきます。
string や retain 指定のプロパティーの場合、セッターでは、新しい値が retain されて、古い値が release されます。もし設定されているのと同じポインターが渡された場合は、何もしない様子なのも特徴的です。
そしてゲッターでは、返す値を retain & autorelease しています。これをブロック内で実行することで、クラスインスタンスの生存を保証しています。
Objective-C クラスインスタンスの場合(copy 指定)
属性として copy が指定されたプロパティの場合、セッター実装の雰囲気が少し変わってきます。
たとえば @property (atomic,readwrite,copy) ClassType* value というプロパティを @synthesize を使って自動実装した場合は、Non-ARC なコードで書くと、次のような実装が自動で生成されます。
セッターの実装(Objective-C クラスインスタンス) [copy] [no-arc]
(void)setValue:(ClassType*)value
{
[_internal lock];
value = [value copy];
[_value release];
_value = value;
[_internal unlock];
}
ゲッターは strong や retain のときと同じです。
セッターは strong や retain のときと少し違っていて、既に設定されているインスタンスが引数で渡されたときであっても、構わず copy して格納しています。
Objective-C クラスインスタンスの場合(assign 指定)
属性として assign が指定されたプロパティの値はとても単純です。
たとえば @property (atomic,readwrite,assign) ClassType* value というプロパティを @synthesize を使って自動実装した場合は、Non-ARC なコードで書くと、次のような実装が自動で生成されます。
セッターの実装(Objective-C クラスインスタンス) [assign] [no-arc]
(void)setValue:(ClassType*)value
{
[_internal lock];
_value = value;
[_internal unlock];
}
ゲッターの実装(Objective-C クラスインスタンス) [assign] [no-arc]
(ClassType*)value
{
__unsafe_unretained ClassType* result;
[_internal lock];
result = _value;
[_internal unlock];
return result;
}
プリミティブ型や複合型のときと同じ振る舞いになるようです。assign の場合、クラスインスタンスの管理は外部に任されているので、ここでわざわざリファレンスカウントを保証する必要がないのでしょう。
もはや値としてはプリミティブ型の void* と同じになるので、そのポインタの値さえ壊れないように保護する形になります。
Objective-C クラスインスタンスの場合(weak 指定)
属性に weak を指定したときの @synthesize の実装の動きは assign のときとほぼ同じようです。
たとえば @property (atomic,readwrite,weak) ClassType* value というプロパティを @synthesize を使って自動実装した場合も、だいたい次のような振る舞いを見せました。
ただ、weak は ARC の機能なので、ここでは ARC コードで紹介します。
セッターの実装(Objective-C クラスインスタンス) [weak] [arc]
(void)setValue:(__unsafe_unretained ClassType*)value
{
[_internal lock];
_value = value;
[_internal unlock];
}
ゲッターの実装(Objective-C クラスインスタンス) [weak] [arc]
(ClassType*)value
{
__weak ClassType* result;
[_internal lock];
result = _value;
[_internal unlock];
return result;
}
ただし ARC の都合からか、ゲッターをこのように実装すると、weak に関する処理が、ゲッター内部でローカル変数に値を受けたときと、戻り値を返した後の 2 度にわたって発生してしまう様子でした。
@synthesize での実装としては、どちらかというと次のような、ゲッター内部では変数に受けない形が近いのかもしれません。
(ClassType*)value
{
@try
{
[_internal lock];
return _value;
}
@finally
{
[_internal unlock];
}
}
ここから窺えるように、@synthesize による自動実装の場合には、より綿密なコードの最適化が図られるのかもしれませんね。
"atomic" の機能を @synthesize を使わないで実装するには
@synthesize を使わない、または使えないような場合は、何らかのロック機能を使って自分で実装する必要があります。
使用できるロックとしては、たとえば NSLock や @synchronized などがあります。これらは @synthesize が自動実装するときに使うロックとは全く別の物になります。
ここでは @synchronized (self) を使って実装する方法について見て行きます。
Non-ARC でのコードの雰囲気は @synthesize を使った場合の実装例で記したので、ここでは ARC 環境で実装する場合に絞って記します。
プリミティブ型と複合型の場合
プリミティブ型や複合型については、ARC 環境でも Non-ARC 環境のときと変わりません。
セッターの実装(プリミティブ型と複合型)
(void)setValue:(DataType)value
{
@synchronized (self)
{
_value = value;
}
}
ゲッターの実装(プリミティブ型と複合型)
(DataType)value
{
@synchronized (self)
{
return _value;
}
}
ゲッターの雰囲気が少し違ってますけど、@synchronized は中括弧で括ったスコープを抜けたときにロックが解除されるため、このようにスコープ内で return を直接使えます。
Objective-C クラスインスタンスの場合(strong 指定)
属性が strong のプロパティは、次のような実装になるようでした。
セッターの実装(Objective-C クラスインスタンス) [strong] [arc]
(void)setValue:(__unsafe_unretained ClassType*)value
{
@synchronized (self)
{
_value = value;
}
}
ゲッターの実装(Objective-C クラスインスタンス) [strong] [arc]
(ClassType*)value
{
@synchronized (self)
{
return _value;
}
}
Non-ARC の時と比べて、セッターもゲッターも少し雰囲気が変わっています。
セッターの方は、インスタンス変数に引数の値を代入してそれだけで終わっています。これだけで、代入する値が retain された後、代入されていた値が release されるという動きが実現されます。条件判定しなくても、代入済みの値が渡されたときには何もしないでくれます。
ゲッターもとても簡単です。ARC では copy 等の名前で始まるメソッドでなければ、戻り値を自動的に autorelease として扱ってくれるので、単純に return でインスタンス変数を返すだけで大丈夫です。
return する前に内部で変数に受けたい場合は、ロックされている内側で、その外側で宣言されている __strong 変数にインスタンス変数を受ける形にすれば大丈夫でしょう。
それと、今回は引数のところに __unsafe_unretained を指定しています。
これは、ARC だと引数に渡したインスタンスが retain されるためで、今回は単に @synthesize で自動実装されたときと動きを似せるために指定しています。
確かに ARC が配慮してやってくれるように、引数のretain する方が安全性は高まると思うので、細かいところが気にならなければ、速度が欲しいときにだけ __unsafe_unretained を付けるという感じで良いかもしれないです。
Objective-C クラスインスタンスの場合(copy 指定)
属性が copy のプロパティは、ARC 環境では、次のような感じになります。
セッターの実装(Objective-C クラスインスタンス) [copy] [arc]
(void)setValue:(__unsafe_unretained ClassType*)value
{
@synchronized (self)
{
_value = [value copy];
}
}
ゲッターの実装(Objective-C クラスインスタンス) [copy] [arc]
(ClassType*)value
{
@synchronized (self)
{
return _value;
}
}
セッターでは、値を代入するときに copy メソッドを呼び出すようにします。こちらも、引数で __unsafe_unretained を指定していますけど、せっかくの ARC 環境なので、ここまで厳密にこだわらなくて大丈夫です。
ゲッターは strong のときと同じで、__strong なインスタンス変数をそのまま return すれば、ARC が自動的に autorelease にしてくれます。
Objective-C クラスインスタンスの場合(weak 指定)
属性に weak を指定したプロパティの実装については、@synthesize による実装の例で紹介しました。
書き方としてはその時と同じですけど、ここでは @synchronized (self) を使って記してみます。
セッターの実装(Objective-C クラスインスタンス) [weak] [arc]
(void)setValue:(__unsafe_unretained ClassType*)value
{
@synchronized (self)
{
_value = value;
}
}
ゲッターの実装(Objective-C クラスインスタンス) [weak] [arc]
(ClassType*)value
{
@synchronized (self)
{
return _value;
}
}
セッターもゲッターも、属性を strong で指定した時と同じコードになりました。
ARC の場合、変数を定義するときに指定したオーナーシップで挙動を変えてくれるので、特別なコードを書かなくても、__weak で定義した変数を適切に扱ってくれます。
それと、こちらも、セッターの引数で __unsafe_unretained を指定していますけど、せっかくの ARC 環境なので、ここまで厳密にこだわらなくて大丈夫と思います。
Objective-C クラスインスタンスの場合(assign 指定)
ところで、属性として assign を指定したプロパティを ARC 環境で実装してみようとしたのですけど、あまり実用的なものが作れませんでした。
たとえば @property (atomic,readwrite,assign) ClassType* value というプロパティ定義しても、自分でメソッドを実装すると、ゲッターが返す値は自動的に retain & autorelease されてしまいます。
プロパティではなくメソッドにして、NS_RETURNS_INNER_POINTER を付けてもその動作は変わらず、試しに NS_RETURNS_RETAINED を付けると表向きは似た動作になるのですけど、これでは Non-ARC で呼び出したときにメモリーリークしそうです。
@synthesize で実装した時のように retain も autorelease もせずにポインタを返す手段があるのかもしれないですけど、今のところ見つけられませんでした。
ARC 環境でも @synthesize で自動実装した時は retain しないそのままのポインターが返せるようなのですけど、そうやって返したポインターを __unsafe_unretained を指定した変数で受けても、そこで一瞬 retain → release されるようでした。
つまりプロパティ参照時に、インスタンスが解放されていれば、その瞬間に deallocated instance で落ちることになります。
Non-ARC 環境のときと同じように、変数で受けても retain が呼ばれなければ、このタイミングを落ちずに素通りできるようになりますけど、解放されたインスタンスが後になって復活することもないですし、ここの挙動をわざわざ再現する価値はなさそうです。
そんな感じで、assign は ARC 環境とはあまり相性が良くなさそうです。
それでも assign を使いたいなら、唯一 nonatomic で使って @synthesize で実装するのが、いちばん理想的な実装なように思えました。
しかしそもそも、ARC 環境でスレッドセーフを考慮するなら atomic, weak が断然良いわけで、そこを敢えて atomic, assign にする価値がどれほどあるかは疑問です。
[ もどる ]