NSOperationってCoreAnimationみたいな派手な機能と違って、あまり解説されていない気がするので、使い方を書いておきます。オーディオのアプリケーションでも、読み込んだオーディオファイルの波形の画像をバックグラウンドで作っておいて、出来上がったら表示するみたいな事にも使えると思いますので。(※2008/5/12 記事にNSInvocationOperation等いろいろ修正を加えています。GCなしにも対応のはず。)
NSOperationを使う
NSOperationのサブクラスのmainメソッドにバックグラウンドで行いたい処理を記述しておき、そのインスタンス(以後、オペレーション)を作成して、NSOperationQueueのインスタンス(以後、キュー)のaddOperationメソッドに渡すと、あとは勝手にバックグラウンドで処理してくれます。キューへ渡した後にオペレーションをいじらなければスレッドセーフだとか気にしなくても良いので、簡単にマルチスレッドの恩恵に預かれるわけです。
では、Cocoaアプリケーションを作成して、使ってみます。
NSOperationのサブクラスを記述します。一瞬で終わってしまう処理なのでNSOperationを使う意味がないような内容ですが、とりあえずサンプルという事で。
mainメソッドをオーバーライドして処理を記述します。
#import <Cocoa/Cocoa.h>
@interface YKOperation : NSOperation {
NSString *testMessage;
}
@property(retain) NSString *testMessage;
@end
@implementation YKOperation
@synthesize testMessage;
- (void)main
{
NSLog(@"%@", self.testMessage);
}
- (void)dealloc
{
[testMessage release];
[super dealloc];
}
@end
つぎはオペレーションを使う側のコントローラクラスです。NSOperationのサブクラスをYKOperationというクラス名にしたので、それをインポートします。それと、NSOperationQueueをインスタンス変数として宣言しておきます。
#import <Cocoa/Cocoa.h>
#import "YKOperation.h"
@interface YKController : NSObject {
NSOperationQueue *gQueue;
}
@end
キューのaddOperation:メソッドにオペレーションを渡します。
@implementation YKController
- (void)awakeFromNib
{
gQueue = [[NSOperationQueue alloc] init];
YKOperation *tOperation = [[YKOperation alloc] init];
tOperation.testMessage = @"Hello, World!";
[gQueue addOperation:tOperation];
[tOperation release];
}
- (void)dealloc
{
[gQueue release];
[super dealloc];
}
@end
InterfaceBuilderでコントローラのクラスをインスタンス化しておき、これを実行してみると、
2008-03-02 12:16:36.084 YKNSOperationTest[4301:4503] Hello, World!
みたいな感じでログに表示されるので、ちゃんと実行されている事が分かります。
キャンセルする
処理の途中でキャンセルさせるには、オペレーションクラスのmainメソッドでキャンセルされたときの動作を記述しておきます。自らのisCancelledを調べるとキャンセルされた事が分かるので、YESになっていれば処理を中断させることができます。逆にmainメソッドの終わりまでキャンセルさせたくない場合には記述する必要はありません。
たとえばオペレーションクラスにおいてこんな感じでmainメソッドを作っておきます。(実際にはNSLogでサブスレッドからログを表示するだけなんてしませんが、とりあえず...。)
- (void)main
{
NSInteger i;
for (i = 0; i < 100; i++) {
NSLog(@"%@ / %d", self.testMessage, i);
if ([self isCancelled] == YES) {
break;
}
}
}
で、実際にキャンセルするときの方法ですが、とにかくすべての処理を止めたいってときは、キューのcancelAllOperationを呼び出します。コントローラ側からこんな感じにします。
- (void)cancelAllOperation
{
[gQueue cancelAllOperations];
}
一つの処理だけを中断させたいときはオペレーションの方のcancelメソッドを呼び出します。現在処理中の処理だけをキャンセルするときはこんな感じでしょうか。
- (void)cancelExecutingOperation
{
for (YKOperation *tOperation in [gQueue operations]) {
if ([tOperation isExecuting] == YES) [tOperation cancel];
}
}
operationsでNSOperationQueueに登録されているオペレーションを取得し、そのisExecutingで処理中かどうかを判断しています。
最大同時処理数と優先度
NSOperationQueueをデフォルトの状態で使っている場合は、addOperationでオペレーションを追加していくと、マシンの性能の許す限りスレッドを分けて同時に処理をしてくれますが、同時に処理する数を制限する事も出来ます。NSOperationQueueのsetMaxConcurrentOperationCount:メソッドでその数を指定します。
例えばNSOperationQueueのインスタンスを作成してから、
- (void)awakeFromNib
{
gQueue = [[NSOperationQueue alloc] init];
[gQueue setMaxConcurrentOperationCount:3];
}
のように3を指定すると、同時に3つまでに制限されます。
ちなみに、処理される順番をコントロールする方法は、優先度(Priority)とdepndencyという2つがあります。
優先度はオペレーションのsetQueuePriorityメソッドで指定でき、以下のような定数が定義されています。
enum {
NSOperationQueuePriorityVeryLow = -8,
NSOperationQueuePriorityLow = -4,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
typedef NSInteger NSOperationQueuePriority;
試してみた感じだと、キューに登録されているオペレーションの中から優先度の高いものが比較的優先されて選ばれて、次の処理が開始されるようです。なので、VeryLowのオペレーションがたくさん登録されて待っている状態でも、Low以上のオペレーションを最後に追加すれば、そちらのほうの処理が先に始まる可能性が高いです。
dependencyを使うとさらにきっちりと処理する順番を決める事が出来ます。あるオペレーションAが終わった後に別のオペレーションBの処理を開始させたいときには、オペレーションBのaddDependencyにオペレーションAを渡して追加しておきます。dependencyはNSArrayで保持されているので、複数登録しておく事が可能です。ただ、これらのオペレーションの依存関係は、キューに追加する前に作っておかなければいけなさそうです。
処理の終了待ち (2008/4/19追記)
NSOperationQueueには、オペレーションを全て処理し終わるまでキュー側のスレッドの処理を止めておくwaitUntilAllOperationsAreFinishedというメソッドがあります。
普通にオペレーションを処理しているときに使っては全く意味ありませんが、今やってる処理が終わるまで待たなければいけない時に使えそうです。もしくはただマルチコアなCPUを活用したいだけの時とか。
キューが解放される時は、オペレーションの処理が全て終わってからdeallocされるみたいなので、あえてwaitUntilAllOperationsAreFinishedを呼ぶ必要は無いですが、アプリケーションの終了時などで、中途半端に処理が終わって困るようなときは効果ありだと思います。
NSInvocationOperation (2008/5/12追記)
と、NSOperationの使い方を長々と書いてきましたが、単純な処理であればNSOperationをサブクラス化する事無く、NSInvocationOperationというNSOperationを継承したクラスを使う事も出来ます。
オペレーションで行わせたい処理を記述してあるオブジェクトと、そのセレクタと、その他渡したいオブジェクトとともにNSInvocationOperationを初期化して、キューに渡します。
以下がそのサンプルになります。
#import <Cocoa/Cocoa.h>
@interface Controller : NSObject {
NSOperationQueue *queue;
}
@end
@implementation Controller
- (void)awakeFromNib
{
queue = [NSOperationQueue new];
NSString *string = @"Hello,World!";
NSInvocationOperation *operation =
[[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(operation:)
object:string];
[queue addOperation:operation];
[operation release];
}
- (void) dealloc
{
[queue release];
[super dealloc];
}
- (void)operation:(NSString *)string
{
NSLog(@"%@", string);
}
@end
Cocoaアプリケーションを作成し、InterfaceBuilderでControllerクラスをインスタンス化しておいて実行すると、ログに"Hello,World!"と表示されると思います。ただこちらの場合、キャンセルするのが面倒くさそうなので、やりっ放しで良いシンプルな処理とかには簡単に使えて良いと思います。
コメントする