LoginSignup
40
40

More than 5 years have passed since last update.

NSOperationを使って並列処理をするシンプルなサンプル

Last updated at Posted at 2014-04-03

NSOperationを使った並列処理をやろうとしてハマったのでメモ。
なんかシンプルなサンプルを探していたのだけど見つからず。
さらに記事に書かれているものも、やりたいことにフォーカスしたものが多くて、最低限必要な処理がどれなのかがイマイチ分からなかったので、必要最低限の状態のものを書いておきます。

サンプルはGithubにあげています。

並列処理と非並列処理

NSOperation(とNSOperationQueue)には、 並列処理非並列処理 の2種類があります。
さらにそれぞれに シングルスレッドマルチスレッド のどちらかで動作するパターンがあり、合計4パターンが存在することになります。

メインスレッド実行 マルチスレッド実行
NSOperationの
非並列モード
A B
NSOperationの
並列モード
C D

今回のサンプルは、 並列処理のサンプルになります。

手順

  • NSOperationのサブクラスを実装する(その際いくつかのメソッドをオーバーライドする)
  • automaticallyNotifiesObserversForKeyクラスメソッド(※1)をオーバーライドする
  • startメソッド(※2)をオーバーライドする(※3)
  • NSOperationQueueinitメソッドで生成する(※4)
  • NSOperationQueueaddOperation:する

※1...automaticallyNotifiesObserversForKeyメソッドはKVOに対応するキーを指定するものです。
※2...startメソッドは、オペレーションがキューに追加された際に自動的に起動されるメソッドです。そしてこの中で処理を書いていきます。
※3...startメソッドをオーバーライドしない場合は、自動的にmainメソッドが呼び出されます。これを利用して、メインの処理をmainに書いておき、startは(必要であれば)準備を行った上でmainメソッドを呼び出すほうがよさそうです。
※4...NSOperationQueue#mainQueueメソッドを利用するとメインスレッドでの処理になり、非並列処理としてオペレーションが実行されます。

KVOを使った状態の通知

NSOperationは、KVOを利用して現在のオペレーションの状態を通知する仕組みを持っています。
(というか、KVOでやる、ってこと)

非並列動作時も同様の仕組みで動作するようなので(未調査)、以下のプロパティを使います。

  • isReady
  • isExecuting
  • isFinished

ほぼそのままのプロパティですね。
終了の通知はisFinishedを監視して行えばいいわけです。

サンプル

ということで、今回のサンプルをGithubにあげておきました。
サンプルのコードを例にしながらメモを書いていきます。

NSOperationのサブクラスを作る

まずはNSOperationのサブクラスを作ります。
(ちなみにNOTNSOperationTestの略ですw)

// NOTOperation.m

@interface NOTOperation ()


@property (assign, nonatomic) BOOL isFinished;
@property (assign, nonatomic) BOOL isExecuting;

@end

////////////////////////////////////////////////////

@implementation NOTOperation

// KVO対象のキーを指定
+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key
{
    if ([key isEqualToString:@"isExecuting"] ||
        [key isEqualToString:@"isFinished"]) {
        return YES;
    }

    return [super automaticallyNotifiesObserversForKey:key];
}

// オペレーション実行
- (void)start
{
    // 処理中のフラグ
    self.isExecuting = YES;

    // スレッド処理開始

    // 分かりやすいようにスリープを入れる
    [NSThread sleepForTimeInterval:2.0];

    // 処理中フラグをオフ
    self.isExecuting = NO;

    // 処理終了フラグ
    self.isFinished = YES;
}

@end

オペレーションを使うクラスを実装

今回はサンプルのためにそれようのクラスを作りましたが、通常はオペレーションを使うクラスに諸々を実装します。

// NOTClient.m

@interface NOTClient ()

@property (strong, nonatomic) NOTOperation *operation;
@property (strong, nonatomic) NSOperationQueue *queue;

@end

//////////////////////////////////////////////////////////

@implementation NOTClient

- (instancetype)init
{
    if (self = [super init]) {
        self.operation = [[NSOperation alloc] init];
        self.queue     = [[NSOperationQueue alloc] init];
    }
    return self;
}

- (void)performMethod
{
    [self.operation addObserver:self
                       forKeyPath:@"isFinished"
                           options:NSKeyValueObservingOptionNew
                           context:nil];

    [self.queue addOperation:self.operation];
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                         ofObject:(id)object
                           change:(NSDictionary *)change
                          context:(void *)context
{
    // do something.
}

@end

処理はstartメソッドに

オペレーションとしての内容はstartメソッドに記述します。
その中で、isExecutingisFinishedプロパティを適切に設定して、現在の状態を更新していく、というのがおおまかな流れになります。
(つまり、状態の遷移を自分で設定していく)

そして利用者はこの状態をKVOで監視することで状態を追える、というわけですね。(だいぶマニュアルw)

ハマったところ

これは完全に初歩的なミスですが、サンプルとして書いていたコードのObserverとして登録していたクラスがリリースされたあとに通知がきて、そのせいでクラッシュしていた、というのがありました。
本来の使い方をしていればあまり出る問題ではないかもしれませんが、シンプルな動作確認を目指したがために、そのあたりをないがしろにしていたのが問題でした。

NSOperationQueueの終わりを検知

waitUntilAllOperationsAreFinishedメソッドで終了を待つ

一番シンプルなのは、waitUntilAllOperationsAreFinishedメソッドを利用して、キューに入っているオペレーションがすべて終了するのを待つパターンです。
スレッドを止めることになるので、メインスレッドでこれをやるとUIをブロックしてしまうので注意が必要。

ただ、処理の記述自体はそのあとに続けて書けるのでシンプルになります。

KVOを使う

これはドキュメントとかで書いてあるわけじゃなくて、KVOを使えばできるかなーという個人的な考えですが、NSOperationQueueにはKVOに準拠しているプロパティが以下の通りあります。

  • operations
  • operationCount
  • maxConcurrentOperationCount
  • suspended
  • name

このうち、operationCountは、現在実行中のオペレーションが終了すると減っていきます。
つまり、この値が0になればキューが空になった=キューが終了した、と見なせそうです。

なので、このプロパティに対してKVOを使って監視をすることで終了を検知することが出来ます。

サンプル

[self.queue addObserver:self
              forKeyPath:@"operationCount"
                 options:NSKeyValueObservingOptionNew
                 context:nil];
40
40
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
40
40