A Day In The Life

とあるプログラマの備忘録

iPhoneアプリ開発時のメモリ管理で気をつけること(マルチスレッド編)

以前iPhoneアプリ開発時のメモリ管理で気をつけることという記事を書いたのですがマルチスレッド時のメモリ管理に関して全く触れてなかったのでまとめてみました。

NSAutoreleasePool はスレッドごとに作成する

処理を別スレッドで実行する場合、スレッドごとに NSAutoreleasePool が必要になります。NSAutoreleasePool の作成を忘れるとメモリリークします。
以下のように main 関数から Sample クラスのインスタンスを生成してメソッドを呼び出している場合、シングルスレッドの場合とマルチスレッドの場合で NSAutoreleasePool の作成タイミングが異なります。

@interface Sample {
}
- (void)hoge;
- (void)foo;
- (void)bar;
- (void)baz;
@end

int main(int argc, char *argv[]) {
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  Sample *s = [[[Sample alloc] init] autorelease];
  [s hoge];
  [pool release];
  return 0;
}
通常のメソッド呼び出し(シングルスレッド)

呼び出し元ですでに NSAutoreleasePool が作成されているので NSAutoreleasePool を作成する必要がありません。

@implementation Sample
- (void)hoge {
  // 普通のメソッド呼び出し
  [self foo];
  [self bar];
  [self baz];
}
- (void)foo {
  // 処理
}
- (void)bar {
  // 処理
}
- (void)baz {
  // 処理
}
@end
マルチスレッドによるメソッド呼び出し

main 関数で作成された NSAutoreleasePool は別スレッドで動いているインスタンスを解放することができません。スレッドごとに NSAutoreleasePool を作成する必要があります。

@implementation Sample
- (void)hoge {
  // 別スレッドでメソッド呼び出し
  [NSThread detachNewThreadSelector:@selector(foo)
                           toTarget:self
                         withObject:nil];
  [NSThread detachNewThreadSelector:@selector(bar)
                           toTarget:self
                         withObject:nil];
  [NSThread detachNewThreadSelector:@selector(baz)
                           toTarget:self
                         withObject:nil];
}
- (void)foo {
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  // 処理
  [pool release];
  [NSThread exit];
}
- (void)bar {
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  // 処理
  [pool release];
  [NSThread exit];
}
- (void)baz {
  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  // 処理
  [pool release];
  [NSThread exit];
}
@end
図で表すとこんな感じです。

図のうすい青色の枠が NSAutoreleasePool の有効範囲です。
シングルスレッド
マルチスレッド
マルチスレッドの場合、スレッドごとに別のスタック領域を使います(ヒープは同じ領域を使います)。このスタック領域ごとに NSAutoreleasePool を作成していればメモリリークは防げるはずです。

メモリリークする例

以下のコードは NSAutoreleasePool を作成してないのでメモリリークします。

@implementation Sample
- (void)hoge {
  [NSThread detachNewThreadSelector:@selector(foo)
                           toTarget:self
                         withObject:nil];
}
- (void)foo {
  // メモリリークします。
  NSArray *array = [NSArray array];
  NSDictionary *dic = [[[NSDictionary alloc] init] autorelease];
}
@end

NSOperation の main メソッドで NSAutoreleasePool を作成する必要があるか?

NSOperation の main メソッドの処理は基本的にマルチスレッドで並列実行されますが main メソッドで NSAutoreleasePool を作成する必要はありません。
以下のクラスで autorelease を使ってメモリがきちんと解放されるか試してみます。

@interface Sample : NSObject {
}
- (void)doSomething;
@end

@implementation Sample
- (void)doSomething {
  NSLog(@"%@", @"Test");
}
- (void)dealloc {
  NSLog(@"%@", @"Dealloc!!!");
  [super dealloc];
}
@end
@interface OperationNormal : NSOperation {   
}
@end

@implementation OperationNormal
- (void)main {
  NSLog(@"Thread:%@", [NSThread currentThread]);
  Sample *s = [[[Sample alloc] init] autorelease];
  [s doSomething];
}
@end

- (void)doSomething {
  NSOperationQueue *queue = [[NSOperationQueue alloc] init];
  OperationNormal *ope1 = [[[OperationNormal alloc] init] autorelease];
  OperationNormal *ope2 = [[[OperationNormal alloc] init] autorelease];
  OperationNormal *ope3 = [[[OperationNormal alloc] init] autorelease];
  [queue addOperation:ope1]; 
  [queue addOperation:ope2];
  [queue addOperation:ope3];
}

実行結果

Sample[1566:6003] Thread:<NSThread: 0x6132030>{name = (null), num = 3}
Sample[1566:1803] Thread:<NSThread: 0x4b188f0>{name = (null), num = 4}
Sample[1566:6103] Thread:<NSThread: 0x4b1af00>{name = (null), num = 5}
Sample[1566:6003] Test
Sample[1566:6103] Test
Sample[1566:1803] Test
Sample[1566:6103] Dealloc!!!
Sample[1566:6003] Dealloc!!!
Sample[1566:1803] Dealloc!!!

1566番のプロセスで6003,1803,6103の3つのスレッドが実行さました。きちんと dealloc メソッドが呼ばれているのがわかります。

続きます。

ブロックやperformSelector:withObject:afterDelay 等まだネタがあるので順次書き足していきます。