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


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

*サンプルは[Github](https://github.com/edom18/NSOperationSample)にあげています。*

##並列処理と非並列処理

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

|   | メインスレッド実行 | マルチスレッド実行 |
|:---|:-----------------:|:-----------------:|
| NSOperationの<br />非並列モード | A | B |
| NSOperationの<br />並列モード | C | D |

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

##手順

* `NSOperation`のサブクラスを実装する（その際いくつかのメソッドをオーバーライドする）
* `automaticallyNotifiesObserversForKey`クラスメソッド(※1)をオーバーライドする
* `start`メソッド(※2)をオーバーライドする(※3)
* `NSOperationQueue`を`init`メソッドで生成する(※4)
* `NSOperationQueue`に`addOperation:`する

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

##KVOを使った状態の通知

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

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

* isReady
* isExecuting
* isFinished

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

##サンプル

ということで、今回の[サンプルをGithubにあげて](https://github.com/edom18/NSOperationSample)おきました。
サンプルのコードを例にしながらメモを書いていきます。

###NSOperationのサブクラスを作る

まずは`NSOperation`のサブクラスを作ります。
*（ちなみに`NOT`は`NSOperationTest`の略ですｗ）*

```objc
// 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
```

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

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

```objc
// 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`メソッドに記述します。
その中で、`isExecuting`と`isFinished`プロパティを適切に設定して、現在の状態を更新していく、というのがおおまかな流れになります。
（つまり、状態の遷移を自分で設定していく）

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


##ハマったところ

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


##NSOperationQueueの終わりを検知

###waitUntilAllOperationsAreFinishedメソッドで終了を待つ

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

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

###KVOを使う

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

* operations
* operationCount
* maxConcurrentOperationCount
* suspended
* name

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

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

####サンプル

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