A Day In The Life

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

NSOperationQueue スレッドと処理の関係

iOS4 になって NSOperationQueue クラスの仕様に追加がありました。以前は並列処理しか出来なかったのですが mainQueue メソッドが追加になり逐次処理も出来るようになりました。変更点も含めて NSOperationQueue の使い方をまとめてみます。
NSOperationQueue クラスの使い方をきちんと理解していると NSOperation クラスの非並列実行モードと並列実行モードの使い方を間違えることも少ないと思います。
NSOperation クラスについては以下の記事で詳しく説明しています。

NSOperationQueue と NSOperation の関係

NSOperationQueue と NSOperation を使うと、バックグラウンドで何か処理をしたい場合に一連の処理をまとめて実行することが出来ます。
NSOperationQueue は処理を実行するためのクラスで NSOperation クラスは実際の処理を定義するためのクラスです。
一連の処理を NSOperation クラスのサブクラスに定義し、その処理を NSOperationQueue が実行してくれます。
NSOperationQueue はメインスレッドだけで処理を実行させる場合とマルチスレッドで処理を実行させる場合があります。また NSOperation は並列実行モードと非並列実行モードがあります。これを図にすると以下のようになります。
NSOperationQueueとNSOperationの関係

  • パターンA
    メインスレッドでのみ動く。
  • パターンB
    処理がメインと別のスレッドで実行される
  • パターンC
    処理がメインスレッドで開始され、さらに別のスレッドでも処理が行われる。
  • パターンD
    処理がメインと別のスレッドで開始され、さらに別のスレッドで処理が行われる。

サンプル用の簡単な処理を作成する

NSOperation クラスのサブクラスを作成して処理を定義します。main メソッドをオーバライドしてそこに処理を書きます。
今回は NSOperationQueue の話に的を絞るので NSOperation は非並列モードで実行します。
※本記事のソースコードiOS4.3 で検証しています。

@interface OperationNormal : NSOperation {
}
@end

@implementation OperationNormal
- (void)main {
  // メインスレッドで実行されているか
  NSLog(@"isMainThread:%d", [NSThread isMainThread]);
  // スレッドの内容をログ出力
  NSLog(@"Thread:%@", [NSThread currentThread]);
}
@end

iPhoneアプリ開発時のメモリ管理で気をつけること(マルチスレッド編)でも書きましたが並列処理する場合でも main メソッド内に NSAutoreleasePool を作成する必要はありません。

処理を並列で実行する

NSOperationQueue を alloc してインスタンスを生成すると NSOperation オブジェクトの処理は並列で実行されます(マルチスレッド実行)。この使い方は iOS4 以前と同じです。

- (void)doSomething {
  NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
  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[1463:1803] isMainThread:0
Sample[1463:6003] isMainThread:0
Sample[1463:6103] isMainThread:0
Sample[1463:6103] Thread:<NSThread: 0x9406a70>{name = (null), num = 5}
Sample[1463:1803] Thread:<NSThread: 0x681be70>{name = (null), num = 3}
Sample[1463:6003] Thread:<NSThread: 0x9406af0>{name = (null), num = 4}

出力結果の[1463:1803]の1463はプロセス番号、1803はスレッド番号です。1803,6003,6103と3つのスレッドで実行されていることがわかります。
並列実行実行結果イメージ
サンプルコードではオペレーションごとにスレッドが作成されましたが必ずオペレーションごとにスレッドが作成されるわけではないようです。

処理をメインスレッドで逐次実行する

NSOperationQueue クラスの mainQueue メソッドでオブジェクトを生成すると NSOperation オブジェクトの処理をメインスレッドで実行することができます。処理はメインスレッドで逐次実行されます(単一スレッド実行)。mainQueue は iOS4,Mac OS X 10.6以降に追加されたメソッドです。

- (void)doSomething {
  NSOperationQueue *queue = [NSOperationQueue mainQueue];
  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[1415:207] isMainThread:1
Sample[1415:207] Thread:<NSThread: 0x4b0e500>{name = (null), num = 1}
Sample[1415:207] isMainThread:1
Sample[1415:207] Thread:<NSThread: 0x4b0e500>{name = (null), num = 1}
Sample[1415:207] isMainThread:1
Sample[1415:207] Thread:<NSThread: 0x4b0e500>{name = (null), num = 1}

スレッド番号207でメインスレッドで直列実行されているのがわかります。
逐次実行実行結果イメージ

isConcurrent メソッド

NSOperation クラスの main メソッドをオーバライドして処理を書いている場合、isConcurrent メソッドの戻り値は無視されます。ただし後述する入れ子実行の場合は isConcurrent メソッドの戻り値で実行結果が変わります。

処理の入れ子実行

NSOperationQueue クラスの currentQueue メソッドを使うと現在実行中のキューオブジェクトが取得できます。このメソッドを使うとオペレーションの処理中にキューを取得して新たなオペレーションを開始することができます。

@interface NestedOperation : NSOperation {   
}
@end

@implementation NestedOperation
- (BOOL)isConcurrent {
  // NOの場合同じスレッドで実行される YESの場合別スレッドで実行される  
  return NO;
}
- (void)main {
  NSLog(@"%@", @"NestedOperation!!!");
  NSLog(@"isMainThread:%d", [NSThread isMainThread]);
  NSLog(@"Thread:%@", [NSThread currentThread]);
}
@end

@implementation OperationNormal
- (void)main {
  NSLog(@"isMainThread:%d", [NSThread isMainThread]);
  NSLog(@"Thread:%@", [NSThread currentThread]);
  // 現在実行中のQueueを取得
  NSOperationQueue *queue = [NSOperationQueue currentQueue];
  NestedOperation *ope = [[NestedOperation alloc] init];
  [queue addOperation:ope];
}
@end

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

NestedOperation クラスの isConcurrent を NO にすると実行中のスレッドで処理が行われます。

addOperationWithBlock メソッド

addOperationWithBlock メソッドを使うと処理を直接ブロックに書くことができます。実行結果は NSOperation のサブクラスを作成して実行した時と同じです。addOperationWithBlock メソッドiOS4,Mac OS X 10.6以降に追加されたメソッドです。

並列実行
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
  NSLog(@"isMainThread:%d", [NSThread isMainThread]);
  NSLog(@"Thread:%@", [NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
  NSLog(@"isMainThread:%d", [NSThread isMainThread]);
  NSLog(@"Thread:%@", [NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
  NSLog(@"isMainThread:%d", [NSThread isMainThread]);
  NSLog(@"Thread:%@", [NSThread currentThread]);
}];

実行結果

Sample[2757:6103] isMainThread:0
Sample[2757:6003] isMainThread:0
Sample[2757:1803] isMainThread:0
Sample[2757:6103] Thread:<NSThread: 0x6112cf0>{name = (null), num = 3}
Sample[2757:6003] Thread:<NSThread: 0x4e111f0>{name = (null), num = 4}
Sample[2757:1803] Thread:<NSThread: 0x4b214d0>{name = (null), num = 5}
逐次実行
NSOperationQueue *queue = [NSOperationQueue mainQueue];
[queue addOperationWithBlock:^{
  NSLog(@"isMainThread:%d", [NSThread isMainThread]);
  NSLog(@"Thread:%@", [NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
  NSLog(@"isMainThread:%d", [NSThread isMainThread]);
  NSLog(@"Thread:%@", [NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
  NSLog(@"isMainThread:%d", [NSThread isMainThread]);
  NSLog(@"Thread:%@", [NSThread currentThread]);
}];

実行結果

Sample[2791:207] isMainThread:1
Sample[2791:207] Thread:<NSThread: 0x4b0e500>{name = (null), num = 1}
Sample[2791:207] isMainThread:1
Sample[2791:207] Thread:<NSThread: 0x4b0e500>{name = (null), num = 1}
Sample[2791:207] isMainThread:1
Sample[2791:207] Thread:<NSThread: 0x4b0e500>{name = (null), num = 1}