Objective-Cでatomicな宣言プロパティがatomicであるとは限らない話 | スマホアプリ開発記

スマホアプリ開発記

スマホアプリやサービス作りで感じたことを書いています。

iPhoneアプリのソースコードレビューをしていたら、こんなコードを見つけました。

self.count++;

アクセサを使って値をインクリメントしています。これ、ぱっと見て問題があると感じますか?


このコードはマルチスレッド環境で動くため、プロパティは以下のようにデフォルトのatomicで(nonatomicを指定せず)定義されています。

@interface Counter: NSObject {
int count;
}
@property(assign) int count;
@end


実は、このコード正しく動きません。

実験してみましょう。以下のように、マルチスレッドで10万回インクリメントしてみます。


@interface AtomicTest : NSObject {
int count;
}
@property(assign) int count;
- (void)increment;
@end


@implementation AtomicTest
@synthesize count;
- (void)increment {
self.count++;
}
@end


int main(int argc, char *argv[])
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
AtomicTest *test = [[AtomicTest alloc] init];

// スレッド呼び出しで10万回インクリメント
for(int i=0; i<100000; i++) {
[NSThread detachNewThreadSelector:@selector(increment) toTarget:test withObject:nil];
}

[NSThread sleepForTimeInterval:5.0];
NSLog(@"count:%d", test.count);

[test release];
[pool drain];
}


10万回インクリメントするわけですから、期待するtest.countの値は10万になるはずですよね。
5回テストしてみた結果は以下のようになりました。

AtomicTest[11783:a0f] count:99982
AtomicTest[11795:a0f] count:99965
AtomicTest[11805:a0f] count:99981
AtomicTest[11816:a0f] count:99989
AtomicTest[11826:a0f] count:99980

どれも10万になっていません。スレッドセーフではないコードのため、値が壊れたのです。

atomic属性のはずのプロパティが、なぜスレッドセーフではなかったのでしょうか?

■ スレッドセーフではない原因


プロパティが自動で生成するgetterとsetterを思い出してみましょう。

@synthesize count;

と定義した時、自動で以下のようなgetterとsetterが生成されます。

- (int)count {
@synchronized(self) {
return count;
}
}

- (void)setCount(int value) {
@synchronized(self) {
count = value;
}
}

さて、始めに出てきたインクリメントのコードは、以下のように展開されます。

self.count = self.count + 1;

これは、さらに次のように展開されます。

[self setCount:[self count] + 1];

つまり、一見すると単にインクリメントという一つの処理が行われているように見えますが、実際は
1. getter呼び出し
2. getterの結果に1を加える
3. setter呼び出し
という3つの処理が行われていたのです。

自動生成されるgetterとsetterはそれぞれの中で排他処理が行われますが、getterとsetterをまたがって排他処理が行われるわけではないのです。そのため、マルチスレッド環境ではタイミングによってはsetterが呼び出される前に別のスレッドからもgetterで値が参照されるという事態が起こり、不正な結果となるわけです。


スレッドセーフなコードを書くには?


インクリメント処理中ロックを取るよう、呼び出し側で@synchronizedで括りましょう。

- (void)increment {
@synchronized(self) {
self.count++;
}
}

テスト結果は

AtomicTest[12140:a0f] count:100000
AtomicTest[12153:a0f] count:100000
AtomicTest[12167:a0f] count:100000
AtomicTest[12186:a0f] count:100000
AtomicTest[12197:a0f] count:100000

問題ありませんね。

C++だと自前でロックするしJavaではsynchronizedを明示するのであまりはまりませんが、Objective-Cではatomic属性ならスレッドセーフとうっかり油断して気づきにくいバグを生みかねません。

参照カウンタやリリースプールなんかもそうですが、Objective-Cの便利機能はブラックボックスの中身を理解していないといけないことが多く、特に初心者にとっては逆にバグの温床となりがちです。注意しましょう。