NSOperationを使った並列処理をやろうとしてハマったのでメモ。
なんかシンプルなサンプルを探していたのだけど見つからず。
さらに記事に書かれているものも、やりたいことにフォーカスしたものが多くて、最低限必要な処理がどれなのかがイマイチ分からなかったので、必要最低限の状態のものを書いておきます。
サンプルはGithubにあげています。
##並列処理と非並列処理
NSOperation
(とNSOperationQueue
)には、 並列処理 と 非並列処理 の2種類があります。
さらにそれぞれに シングルスレッド と マルチスレッド のどちらかで動作するパターンがあり、合計4パターンが存在することになります。
メインスレッド実行 | マルチスレッド実行 | |
---|---|---|
NSOperationの 非並列モード |
A | B |
NSOperationの 並列モード |
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にあげておきました。
サンプルのコードを例にしながらメモを書いていきます。
###NSOperationのサブクラスを作る
まずはNSOperation
のサブクラスを作ります。
(ちなみにNOT
はNSOperationTest
の略です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
メソッドに記述します。
その中で、isExecuting
とisFinished
プロパティを適切に設定して、現在の状態を更新していく、というのがおおまかな流れになります。
(つまり、状態の遷移を自分で設定していく)
そして利用者はこの状態を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];