SSブログ

光学薄膜設計ソフトの設計 その14「NSOperation実験」 [考え中 - 光学薄膜設計]

さて、マルチコアを使い倒すためにはthreadを生成しなければならない。それを簡単にするためのNSOperationのお勉強。このFoundationのクラスは10.5 Leopardから使えるようになった。

Cocoaアプリでマルチコアに対応させるためにはこれまでNSThreadを使っていた。NSThreadはthreadの同期の機能はなく、自分でNSLockなどを使って実装する必要があった。注意深く作法を守らないと、つまらない理由のせいでデッドロックから抜け出せないコードになってしまうなんてことがよくある。

そのthread生成から同期、特に終了処理(子threadが全部終わるまで待つ)というのを肩代わりしてくれるのがNSOperationらしい。

マルチコアを使う用途だけでなく、ある程度時間のかかる処理をマルチスレッド化する場合にも使えるらしいので、以前数値計算のためにNSThreadを使ったけどその代わりに使える。

NSOperationを使うにはいくつかの方法があるみたい。 NSOperationはそれほど大掛かりではない抽象クラスで、正統的な使い方はNSOperationのサブクラスを作って、それに処理を書くというやりかた。ほかにも実態を持ってるNSInvocationOperationというのがあって、
- (id)initWithTarget:(id)target selector:(SEL)sel object:(id)arg
を使えば、わざわざサブクラスを作らなくてもいい。これはちょうどNSThreadやNSTimerで特定のメソッドを実行するやり方と同じ。

とりあえず、まず一番簡単な使い方を勉強してみる。

・NSOperationとNSOperationQueueの基本

NSOperationのサブクラスを作って、mainメソッドの中にやりたい処理を書く。そして(NSOperationで定義された)startメソッドを呼ぶ。startメソッドは何らかの自前の処理をした後mainメソッドを呼ぶということをしているのだろう。これを使って別threadで作業するにはNSOperationQueueを使う。従って、一番簡単な使い方は
  1. NSOperationのサブクラスを作ってmainメソッドに処理を書く
  2. そのサブクラスのインスタンスを作る
  3. NSOperationQueueのインスタンスを作る
  4. NSOperationのサブクラスのインスタンスをNSOperationQueueに登録する
  5. NSOperationQueueに-waitUntilAllOperationsAreFinishedメソッドを投げる
とすればいい。もちろんNSOperationのサブクラスのインスタンスは複数個登録できる。NSOperationQueueへのNSOperationのサブクラスの登録はaddOperation:メソッドを使う。登録はできるけどremoveOperation:みたいなメソッドは無いみたい。別に欲しいわけじゃないけどね。
NSOperationQueueは-waitUntilAllOperationsAreFinishedが呼ばれると、これはたぶんだけど
  1. NSLockを作って排他制御の準備をする
  2. threadをdetachする
  3. (ガベージコレクション環境でなければ)NSAutoreleasePoolを作る
  4. 登録されたNSOperationのインスタンスにstartメソッドを投げる
ということをしているらしい。
NSOperationQueueのwaitUntilAllOperationsAreFinishedメソッドから抜けてきたときは全部のNSOperationの処理は終わっている。
NSOperationのサブクラスの書き方やNSOperationQueueとの関係はFoundation Frameworkによくあるパターンなので慣れていればわかりやすい。逆に、初めての人はmainメソッドとstartメソッドの関係がピンと来なかったりするかも知れない。

・NSOperationインスタンス間の依存性設定

もちろんNSOperationのサブクラスのmainメソッドはthread safeに書かなければいけないのは当然だけど、複数個のインスタンスの間の依存関係があってもかまわない。例えばinstance1はinstance0の計算結果を利用していてinstance0が終わらないとinstance1が始められないとき
	[insntance1 addDependency:instance0];

としてやれば、instance0がisFinishedにYESを返すまでinstance1はブロックされる。おそらく、instance0内部のdependenciesというNSMutableArrayに登録されたインスタンス全部がisFinishedにYESを返すまでisReadyがNOのままで、NSOperationQueueはisReadyにYESを返すインスタンスから順番に実行するのだろう。

NSOperationQueueは登録されたインスタンスの数だけthreadを作ってisReadyがNOの間はブロックしておけば簡単だけど、たぶん効率を考えるとNSOperationQueueはコアの数を超えないthreadを作ってisReadyにYESを返したものを順にthreadに投入する必要がある。こういう処理は面倒なのでNSOperation+NSOperationQueueを使うのが便利。しかしこれ以上の複雑な依存関係は定義できない。また、依存関係がループになってる場合は当然デッドロックを起こすので、ループが無いことを保証してやる必要がある。

また、setMaxConcurrentOperationCount:メソッドでthreadの数を指定できる。デフォルトのNSOperationQueueDefaultMaxConcurrentOperationCount(enumで-1になってる。なげーよ)だと、コアの数とその時点でのシステムの負荷から決定されるらしい。

・キャンセル処理

また、mainの中はキャンセルに対応するように書いておいた方がいいとなっている。キャンセルに対応するには、mainの中で定期的に自分自身にisCanceledを投げてYESが帰ってきたらすぐ終わらせるようにする。例えば
- (void)main
{
    Boolean	loop = YES;
    while (loop) {
          if ([self isCanceled]) {
            //    ローカルにアロケートされたメモリなどのクリーンアップ
               break;
            }
        //    通常処理
    }
}

などとしておく。今考えているNSOperationQueueを使う方法ではNSOperationQueueのインスタンスにcancelAllOperationsを投げれば動いているNSOperationのインスタンスにcancelを投げ、動かす前のインスタンスはなにもされないで、帰ってくる。でもアプリケーションのmain threadでNSOperationQueueのwaitUntilAllOperationsAreFinishedを呼んでしまうと、終わるまで帰ってこないのでキャンセルできない。結局NSOperationQueue専用のthreadを自前でdetachしないといけないような気がするけど。

・それでは実験

実験のためにこんなNSOperationのサブクラスを作る。ヘッダ"ConcreteOperation.h"は
@interface ConcreteOperation : NSOperation {
    NSString    *operationName;
    int         maxc;
    int         count;
}
- (id)initWithName:(NSString *)name andMaxCount:(int)maxCount;
@end
で、インプリメンテーションファイル"ConcreteOperation.m"は
#import "ConcreteOperation.h"
@implementation ConcreteOperation
- (id)initWithName:(NSString *)name andMaxCount:(int)maxCount
{
    self = [super init];
    operationName = [name retain];
    maxc = maxCount;
    count = 0;
    return self;
}

- (void)dealloc
{
    [operationName release];
    [super dealloc];
}

- (void)main
{
    count = 1;
    while (count < maxc) {
        NSLog(@"from %@ count = %d", operationName, count);
        [NSThread sleepUntilDate:
                    [NSDate dateWithTimeIntervalSinceNow:1.0]]; // (1)
        count ++; // (2)
    }
    NSLog(@"%@ now exits", operationName);
}
@end
このConcreteOperationクラスはNSOperationのサブクラスで、mainメソッドを上書きしている。mainメソッドの(1)で、1秒スリープして(2)でカウンタをインクリメントする。maxcを超えるとmainメソッドを抜ける。これだけ。キャンセル処理は書いていない。init...には名前とmaxcを設定している。
こいつをこんなmainファイルで呼んでみる。
#import <Foundation/Foundation.h>
#import "ConcreteOperation.h"

int main (int argc, const char * argv[]) {
   NSAutoreleasePool   *pool = [[NSAutoreleasePool alloc] init];
    ConcreteOperation   *op1 = [[ConcreteOperation alloc]
                            initWithName:@"Op1" andMaxCount:2]; // (3)
    ConcreteOperation   *op2 = [[ConcreteOperation alloc]
                            initWithName:@"Op2" andMaxCount:1];
    ConcreteOperation   *op3 = [[ConcreteOperation alloc]
                            initWithName:@"Op3" andMaxCount:3];
    NSOperationQueue    *opque = [[NSOperationQueue alloc] init];
    int numOfCores = [[NSProcessInfo processInfo] processorCount];

    NSLog(@"number of cores = %d\n", numOfCores);
    [opque addOperation:op1]; // (4)
    [opque addOperation:op2];
    [opque addOperation:op3];
    [opque waitUntilAllOperationsAreFinished]; // (5)
    NSLog(@"all finished.");
//    release all objects here...
    [pool drain];
    return 0;
}
ConcreteOperationを3つ作って(3)、NSOperationQueueに登録して(4)、waitUntilAllOperationsAreFinishedを呼んだ(5)。

・その結果

[Session started at 2008-04-24 22:05:45 +0900.]
2008-04-24 22:05:45.761 TestNSOperation[625:10b] number of cores = 2
2008-04-24 22:05:45.763 TestNSOperation[625:1503] from Op1 count = 0
2008-04-24 22:05:45.763 TestNSOperation[625:1603] from Op2 count = 0
2008-04-24 22:05:45.803 TestNSOperation[625:2c03] from Op3 count = 0
2008-04-24 22:05:46.764 TestNSOperation[625:1503] from Op1 count = 1
2008-04-24 22:05:46.764 TestNSOperation[625:1603] Op2 now exits
2008-04-24 22:05:46.804 TestNSOperation[625:2c03] from Op3 count = 1
2008-04-24 22:05:47.765 TestNSOperation[625:1503] Op1 now exits
2008-04-24 22:05:47.805 TestNSOperation[625:2c03] from Op3 count = 2
2008-04-24 22:05:48.806 TestNSOperation[625:2c03] Op3 now exits
2008-04-24 22:05:48.807 TestNSOperation[625:10b] all finished.

The Debugger has exited with status 0.
コアの数は2つなので最初のふたつだけが実行されるのかと思ったら3つとも始まった。コアの数にかんけー無いやん、うそつき。

さきの(5)の前に
    [op3 addDependency:op1];
を入れて、つまりop3はop1に依存すると設定してそのまま実行すると、
[Session started at 2008-04-24 22:51:37 +0900.]
2008-04-24 22:51:37.492 TestNSOperation[818:10b] number of cores = 2
2008-04-24 22:51:37.495 TestNSOperation[818:1503] from Op1 count = 0
2008-04-24 22:51:37.495 TestNSOperation[818:1603] from Op2 count = 0
2008-04-24 22:51:38.501 TestNSOperation[818:1503] from Op1 count = 1
2008-04-24 22:51:38.501 TestNSOperation[818:1603] Op2 now exits
2008-04-24 22:51:39.502 TestNSOperation[818:1503] Op1 now exits
2008-04-24 22:51:39.506 TestNSOperation[818:1603] from Op3 count = 0
2008-04-24 22:51:40.508 TestNSOperation[818:1603] from Op3 count = 1
2008-04-24 22:51:41.510 TestNSOperation[818:1603] from Op3 count = 2
2008-04-24 22:51:42.510 TestNSOperation[818:1603] Op3 now exits
2008-04-24 22:51:42.511 TestNSOperation[818:10b] all finished.

The Debugger has exited with status 0.


おお、その通りになった(読み辛いけどOP1がexitしてからOP3が始まってる)。この依存指定を書いた行を消して、こんどはそのかわりに
    [opque setMaxConcurrentOperationCount:numOfCores]; // (6)
としてみる。コアの数(今のiMacでは2)までしかthreadは作られないはず。で、
[Session started at 2008-04-24 22:51:51 +0900.]
2008-04-24 22:51:51.813 TestNSOperation[846:10b] number of cores = 2
2008-04-24 22:51:51.817 TestNSOperation[846:1503] from Op1 count = 0
2008-04-24 22:51:51.825 TestNSOperation[846:1603] from Op2 count = 0
2008-04-24 22:51:52.819 TestNSOperation[846:1503] from Op1 count = 1
2008-04-24 22:51:52.846 TestNSOperation[846:1603] Op2 now exits
2008-04-24 22:51:52.850 TestNSOperation[846:1603] from Op3 count = 0
2008-04-24 22:51:53.819 TestNSOperation[846:1503] Op1 now exits
2008-04-24 22:51:53.852 TestNSOperation[846:1603] from Op3 count = 1
2008-04-24 22:51:54.853 TestNSOperation[846:1603] from Op3 count = 2
2008-04-24 22:51:55.853 TestNSOperation[846:1603] Op3 now exits
2008-04-24 22:51:55.854 TestNSOperation[846:10b] all finished.

The Debugger has exited with status 0.
依存はないけど、Op2が終わってからOp3が始まった。これはこれで正しい。結局コアの数だけthreadを作るなら(6)を書かなければいけないということ? まあそれならそれでいいけど、こういう挙動はちょっとOSが上がるとあっさり変わったりするからなあ。

ということで、NSOperartionとNSOperationQueueを使うとNSThreadのdetachやNSLockをあらわに見ることなく、マルチコアが使える。これは簡単でいい。前考えたOTFTargetとOTFEvaluatorは光学薄膜マトリクスを計算するクラスのサブクラスで、その計算クラスはさらにNSOperationのサブクラスということになって、OTFTargetとOTFEvaluatorはmainメソッドで自分の計算をやる、ということになる。そしてNSOperationQueueはOTFMeritFunctionが持つ。OTFEvaluatorもマルチコアで動作できるようにOTFMeritFunctionと同じレベルのOTFOpticalEvaluationとでもいうようなクラスを作ってNSOperationQueueを保持することにしよう。これでOKでしょ、きっと。

NSOperationが簡単でおもしろいので、またごちゃごちゃ書いてしまったな。新しいことを勉強するのはボケ防止にはちょうどええけど。
nice!(0)  コメント(3)  トラックバック(0) 

nice! 0

コメント 3

decafish

記述の一部に間違いがありました。修正と追記をhttp://decafish.blog.so-net.ne.jp/2009-08-07-2http://decafish.blog.so-net.ne.jp/2009-08-08に書きましたのでご確認ください。
decafish
by decafish (2009-08-09 09:58) 

keita

たいへん参考になりました。
by keita (2010-05-09 17:47) 

decafish

コメントありがとうございます。
今後ともよろしくお願いします。
by decafish (2010-05-09 20:19) 

コメントを書く

お名前:
URL:
コメント:
画像認証:
下の画像に表示されている文字を入力してください。

トラックバック 0

献立04/24むりやり11連休で帰宅 ブログトップ

この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。