Rubyと比べながらBlocksをいじってみたりBlocksでNSArrayにmapメソッドを生やしてみたりしてきたので、そろそろGrand Central Dispatch(GCD)も試してみる。あんま関係ないけど、グランド・セントラル・ディスパッチってなんか必殺技っぽいよね。じゃあ一緒に高らかに叫んでみようか。せーの、グランド!セントラル!ディスパッチ!!

GCDってなにさ

ドキュメント嫁。

…だけだと流石に不親切なので、一応簡単に説明すると、APIを通してぽんぽん処理をqueueにつっこんでってやると、ランタイムの方でそれを上手いこと並列実行しといてやるよ!安心しろチェリーボーイ共、スレッドのことは俺が面倒見てやるぜ!って仕組み。そんな口調なのかどうかはわかんないけど、まぁ大体そんな感じ(適当)。

例によってRubyと比較

まぁ、こんなコードがあったとします。

# ruby
f = lambda {
  puts "0.25秒後から本気出す"
  sleep 0.25
}
t = Time.now
20.times do
  f.call
end
p Time.now - t

んで、それを例によってObjCでBlocksを使って書くとこんなコードになります。ちなみにどっちも別にBlocksやlambdaでやる必要は無いんだけど、この後のコードと比較の為にわざとそうしてるのでスルーしておくれやす。

# ObjC
void (^f)(void) = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
};
NSDate d = [NSDate date];
unsigned int i = 20;
while (i--) {
  f();
}
NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

まぁ見ての通り、標準出力に一言言ってから0.25秒sleepするだけの簡単なお仕事を20回ほどやってもらってます。あたり前のことだけど、0.25*20で5秒+αくらいの時間がかかるし、0.25秒っつってんのに5秒待たせるとか相当いい加減なやつだ。

んで、こんな風に、それぞれの処理が独立してるけど一個一個は結構時間かかる、みたいなのは、並列に実行させちゃったらいいんじゃね、みたいなことを偉い人は言いました。

# ruby
f = lambda {
  puts "0.25秒後から本気出す"
  sleep 0.25
}
t = Time.now
20.times do
  Thread.new { f.call }
end
(ThreadGroup::Default.list - [Thread.current]).each{|th|th.join}
p Time.now - t

まだ若干怠けてるけど、まぁ0.3秒行かない程度で終わる。5秒に比べたら一瞬みたいなもんだよね。

じゃあ次はそれをObjCで…と言いたいところなんだけど、ObjCで上のRubyのコードをNSThreadってクラスを使って書こうとすると、割と面倒い。特定のコンテキストを別スレッドで実行しようと思うと、detachするのにtargetとselector、つまりスレッドで実行されるオブジェクトとそいつから呼び出されるメソッドがなくちゃいけない。んでもって、作ったスレッドを自分で管理しなきゃいけない。一応適当なサンプルは書いたけど、あんまりこれをObjCで自前で書くことは無いと思う(理由は後述する)。

さて、GCDです

最初の方に言ったけど、大分ざっくり言うとGCDってのは「処理のブロックをキューにつっこんでってやると裏で上手いこと並列に処理してくれる」ものです。要は並列処理のめんどい感じを多少楽にしてくれるのがGCDの兄貴だってことです。兄貴なのか姉貴なのかは知りませんが。どっちかというと僕はお姉さんが好きですがどうでもいいことです。

# ObjC
dispatch_block_t block = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
//  NSLog(@"%@", [NSThread currentThread]);
};
//NSLog(@"%@", [NSThread currentThread]);

NSDate d = [NSDate date];

// ここからGCD登場
dispacth_group_t group = disptach_group_create();
disptach_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
unsigned int i = 20;
while (i--) {
  disptch_group_async(group, queue, block);
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// ここまでGCDのお仕事

NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

はい。Rubyの方をThreadを使って書き直したときと同じく、0.3秒行かないくらいの時間でさくっと処理してくれました。コメントアウトしてるのを戻せば、ちゃんと別々のスレッドで動いてるのも確認できると思います。ちなみにObjCの文法で書いてるとこを削れば普通にCでも使えます。使いたいときは#include <dispatch/dispatch.h>してください。

変わったところを解説すると、まずブロックの型がdispatch_block_tに変わってる。これは後で出てくるdispatch_group_asyncの引数の型なんだけど、void (^)(void)、つまり何も取らず何も返さないブロックって定義になってるので実はさっきと何も変わってない。

次にdisptach_group_createをしてgroupを作ってるけど、これはまぁ名前通り非同期に実行する処理をグルーピングするためのもの。Rubyの方でもThreadGroupが出てきたけど、あれと一緒で最後のdispatch_group_waitで一連の処理が全部終了するまで待ってやる為に使う。今回は全部の処理が終わるまでの時間が見たかったのと、アプリケーションとかじゃない普通のCUIのコマンドとして作ったときに何も考えずに非同期処理させるとdispatchしたのが終わる前にmainが終わっちゃうのでwaitする必要があったけど、GUIアプリケーションとかデーモンとかだとその心配はないのでグルーピングせずに単にdispatch_asyncしちゃってもいい。

んでここからが本質、dispatch_get_global_queueとdispatch_async(またはdispatch_group_async)。といっても別に大したことではなくて、

  1. queueを用意します
  2. dispatch_asyncにqueueとblockを渡します
  3. あとは裏でよしなにやってくれます

以上。中では「システムの負荷を見てスレッドを作るか待つか決める」「スレッドが一個も空いてなければ作るけど、さぼってるやつがいたら再利用する」「あっちこっちから放り込まれたブロックをどのスレッドに割り当てたら効率良いか考える」とか色々やってんだけど、使う側としてはそんなこと気にする必要無いし、それどころかスレッドが作られてることすら隠蔽されてる。やったのは単に関数にブロックを渡しただけ。ゆとりの僕でもできる簡単なお仕事です。ちなみにdispatchしてwaitするあたりの処理を続けて何回も実行すると、ちゃんとスレッド再利用してるのが確認できる。

他にも、globalって名前が付いた関数があるからにはglobalじゃないqueueを作る関数もあるとか、asyncって名前が付いた関数があるからにはsyncして実行する関数もあるとか、メインスレッドで動作するqueueがあるとか、さっきのコードではわざわざforループ回したけどループにはループ専用のdispatch_applyがあるとか、まぁ色々あるんだけど、Xcodeのあのアホみたいに使い辛いドキュメントビューワの検索窓にdispatch_って入れてやるといっぱい出てくるので見てみてくれればいいかと思います。

でもそれCじゃん

ええ。ここまでは誰がどうみてもCの関数、っていうかさっきも書いた通り実際<dispatch/dispatch.h>をincludeすればCでも使えるAPIです。いやさ、確かにObjCの文法はキモいけどさ、せっかくObjCで書いてるのにCの関数使うってどうなのよって?ご心配なく。ObjCならObjC流に実装する方法はもちろんある。

「並列実行」「どんどんキューにつっこむ」「スレッドの面倒はキューが見てくれる」あたりで、LeopardまでのOSXとかiPhoneSDKとかでアプリを書いたことある人は「それNSOperationとNSOperationQueueでできるじゃん」と思ったはず。これもキューにオペレーションオブジェクトをどんどんつっこんで行けば非同期でよしなにやってくれるクラスで、それ自体はGCDやBlocksとは関係なく前から使える。ので、もともとあんまり自前でNSThreadを作ったり管理したりはやったことなかった。じゃあObjCだと何も変わらないじゃんと思ったけど、Blocksが導入されたことでNSArrayやNSDictionary同様便利な機能が追加されているという。

# ObjC
void (^block)(void) = ^{
  NSLog(@"あと0.25秒だけ寝させてー");
  [NSThread sleepForTimeInterval:0.25];
//  NSLog(@"%@", [NSThread currentThread]);
};

//NSLog(@"%@", [NSThread currentThread]);
NSDate d = [NSDate date];

// こっからNSOpera(ry
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
unsigned int n = 20;
while (n--) {
  [queue addOperationWithBlock:block];
}
[qeueu waitUntilAllOperationsAreFinished];
// ここまでNSOpera(ry

NSLog(@"%f", [[NSDate date] timeIntervalSinceDate:d]);

おお、これは便利だな。Blocsを使わない場合は事前にNSOperationクラスを継承して独自のOperationクラスを作っとくとか、NSInvocationOperationを使うとかしてたところを、-[NSOperationQueue addOperationWithBlock:]を使えばブロックを渡してやるだけで非同期実行できちゃう。ブロックをひとまとめにしてオペレーションにしてくれるNSBlockOperationってクラスや、オペレーションが終了した後に実行される処理をブロックで設定できる-[NSOperation setCompletionBlock:]ってメソッドが追加されてて、「処理を一括りにして並列実行する」コードが大分書きやすくなっている。

MacRuby

RubyとObjCの話をしてるのにMacRubyさんを完全にスルーするという素敵なプレイをやってのけてきたわけですが、実はMacRubyさんはとっくにGCDに対応してやがります(MacRuby » An Introduction to GCD with MacRuby)。凄いな、ほんと、どこまで行くんだろう。

ついでに

Blocks入門NSArrayにmap生やしたのとこの記事のコードをのサンプルはgistに上げてみたので、まぁ一応一通り動く例になってるはず。あーあと簡易ベンチマーククラスみたいのも作ってみたので入れといた。適当なので実用するのはおすすめしません。Snow Leopardでrubyとrakeが入ってる環境なら、cloneしてきてrakeすればコンパイルできるはず。

参考記事