なぜブロックなのか? - 前編 [プログラミング]
OS XとiOSでマルチスレッドなアプリを作るためにGCD(Grand Central Dispatch、Appleによる日本語pdf)というCから呼べる機能がOS Xでは10.6から追加された。これはdispatchというライブラリとC言語に「ブロック(Appleによる日本語pdf)」という仕様を追加して実現されている。GCDは便利なんだけど、Cの文法にいわゆる「クロージャ」というCとは異質な仕様を追加してまでやるべきことなのか?という疑問が湧いた。
「ブロック」について今日ともう1回かけて考えてみる。今日は例を使ってシングルスレッドなコードをGCDとブロックを使ってマルチスレッドにすること、そしてもしGCDがブロックを使わないとしたらどうなるか、ということを考えてみる。
OS Xではマルチスレッドなアプリのために
まずNSOperation を導入して、その概念をもっと一般的に利用できるように GCD が作られて、 NSOperation 自身は GCD を使って実装し直された、と言う風な感じにどうも僕には思える。実際にどうなっているのかはわからないけど。
もちろんNSOperationやGCDを使わずに、これまでからある
しかしせっかく簡単にできる手段が提供されているので、使わない手はない。とくにGCDはCの仕様を拡張してまで鳴り物入りで導入されたので注目度も高かった。でも僕はまだ使ったことがない。なんでかというとCだけで(あるいはC++で)OS Xアプリを書くことがないので、NSOperationを使えばすむからである。
例は何でもいいんだけど、こないだ書いたばかりのマトリクスとベクトルの積をいっぱいやる、というパターンを考えてみる。こんなのだった。
こないだの評価では、このコードはvDSPを使ったマトリクス×マトリクスと実行速度はとんとんだった。コンパイラの進歩におんぶにだっこ、というか、コンパイラが最適化しやすいようにコードを書くのがソースレベルのピープホール最適化である、という時代になったんだととりあえず思っている。
GCDではディスパッチキューをとってきて、非同期に実行させたい作業をブロックにしてそれに渡せばいい。
実際にどんなコードになるかと言うと
これを実行すると、ブロックは別スレッドで実行されて、このメソッドはそれが終わる前に返ってくる。
ブロックはレキシカルスコープなので、メソッドの仮引数や自動変数などはそのまま書けて、ようするに別スレッドで実行させたいコード部分をdispatch_async()の第2引数として囲んでしまえばいい。非常に簡単。
dispatch_wob_async()関数のプロトタイプは
これを使うためにさっきのコードを変更する。
まず、refConを定義して、実行のための関数を作る。こんな感じ。
そしてこれを使ってさっきのメソッドを書き換えると
さっきのブロックを使うGCDに比べると、コールバック関数では呼び出し元の変数が見えなくなるせいで、多くの作業が必要になって非常に煩わしい。しかしこれまでのCではこれが普通だった。pthreadを使うとすると、これと全く同じことをする必要があるし、例えばCore Foundationの関数の多くはコールバックが設定できて、細かな制御が可能になっているけど、それもこれと同じことをしなければいけない。
逆に、この「ブロックを使わないGCD」はpthreadを使うのとそれほど手間は変わらないので、pthreadでいいじゃん、ということになってしまう。つまりこのGCDは実質的に単なるスレッドプールライブラリと同じで、それほどありがたみはない、ということである。
「ブロック」について今日ともう1回かけて考えてみる。今日は例を使ってシングルスレッドなコードをGCDとブロックを使ってマルチスレッドにすること、そしてもしGCDがブロックを使わないとしたらどうなるか、ということを考えてみる。
1.0 はじめに
いまやろうとしているポリゴン描画アプリでは、レスポンスが重要なのでマルチスレッドに書きたいと思っている。OS Xではマルチスレッドなアプリのために
- NSOperation
- GCD(Grand Central Dispatch)
まずNSOperation を導入して、その概念をもっと一般的に利用できるように GCD が作られて、 NSOperation 自身は GCD を使って実装し直された、と言う風な感じにどうも僕には思える。実際にどうなっているのかはわからないけど。
もちろんNSOperationやGCDを使わずに、これまでからある
- NSThread(Cocoaスレッド)
- pthread(POSIXスレッド)
- NSWorkspace
- forkとexec(unixシステムコール)
しかしせっかく簡単にできる手段が提供されているので、使わない手はない。とくにGCDはCの仕様を拡張してまで鳴り物入りで導入されたので注目度も高かった。でも僕はまだ使ったことがない。なんでかというとCだけで(あるいはC++で)OS Xアプリを書くことがないので、NSOperationを使えばすむからである。
1.1 なぜブロックなのか?
しかしGCDを使えるようにするために、わざわざCに新しい構文を独自拡張する必要があったのか?という疑問が湧いた。「なぜブロックなのか?」を僕なりに考えてみた。例を使って説明してみる。例は何でもいいんだけど、こないだ書いたばかりのマトリクスとベクトルの積をいっぱいやる、というパターンを考えてみる。こんなのだった。
const int dimension = 4; typedef double vector[dimension]; typedef struct matrixStruct { vector m[dimension]; } matrix; double innerProduct(vector *v1, vector *v2) { double ret = 0.0; for (int i = 0 ; i < dimension ; i ++) ret += (*v1)[i] * (*v2)[i]; return ret; } void mvProduct(vector *result, matrix *mat, vector *v) { for (int i = 0 ; i < dimension ; i ++) (*result)[i] = innerProduct(&(mat->m[i]), v); } - (void)projectiveTransform:(NSInteger)length forVectors:(vector *)vectors toVectors:(vector *)results { matrix projmat; for (int i = 0 ; i < dimension ; i ++) for (int j = 0 ; j < dimension ; j ++) { // マトリクスのセットアップ } for (int n = 0 ; n < length ; n ++) // マトリクスとベクトルの積 mvProduct(results + n, &projmat, vectors + n); }同次座標のベクトルの配列が渡されて、内部でマトリクスを設定してそれをかけ算している。mvProduct()という関数はベクトルとマトリクスの積を作る関数で、その中でさらにベクトルの内積を作る関数であるinnerProduct()を呼んでいる。わざわざ内積に置き換えてマトリクスの積を計算しているのはわかりやすさのためだけで、最終的にはコンパイラがインライン展開してしまうだろう、という読みでこう書いてある。
こないだの評価では、このコードはvDSPを使ったマトリクス×マトリクスと実行速度はとんとんだった。コンパイラの進歩におんぶにだっこ、というか、コンパイラが最適化しやすいようにコードを書くのがソースレベルのピープホール最適化である、という時代になったんだととりあえず思っている。
1.2 GCDを使ってみる
最後のマトリクスのかけ算が非同期に行われてもいい、つまりこのメソッドではかけ算の結果を使わない、として、別のスレッドで実行することを考える。GCDではディスパッチキューをとってきて、非同期に実行させたい作業をブロックにしてそれに渡せばいい。
実際にどんなコードになるかと言うと
- (void)projectiveTransform:(NSInteger)length forVectors:(vector *)vectors toVectors:(vector *)results { __block matrix projmat; // マトリクスのセットアップ(省略) // グローバルキューをとってくる dispatch_queue_t queue = dispatch_get_global_queue (DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(queue, ^(void){ for (int n = 0 ; n < length ; n ++) // マトリクスとベクトルの積 mvProduct(results + n, &projmat, vectors + n); }); }dispatch_get_global_queue()関数で、キューをもらってきてそれにdispatch_async()関数で実行させている。dispatch_async()関数のふたつめの引数が実行するブロックで、マトリクスとベクトルの積をするループをそのまま無名ブロックとして渡している。
これを実行すると、ブロックは別スレッドで実行されて、このメソッドはそれが終わる前に返ってくる。
ブロックはレキシカルスコープなので、メソッドの仮引数や自動変数などはそのまま書けて、ようするに別スレッドで実行させたいコード部分をdispatch_async()の第2引数として囲んでしまえばいい。非常に簡単。
1.3 GCDがブロックを使わないとするとどうなるか
もし、GCDがブロックを使わなかったとしたらどうなるだろう。Cなのでいわゆるコールバック関数とrefConを使うという形になるだろう。そういう(ブロックを使わない)キューの型をdispatch_wob_queue_t、キューを返す関数をdispatch_get_global_wob_queue()、実行する関数をdispatch_wob_async()としよう。dispatch_wob_async()関数のプロトタイプは
typedef void (*performFunction)(void *); dispatch_wob_async(dispatch_wob_queue_t queue, performFunction func, void *refCon);のようになるだろう。実行関数へのポインタ(コールバック関数と言う)と、その関数へ渡すデータをひとまとめにした構造体へのポインタrefCon(プロトタイプでは汎用のvoidポインタになっている)を受けて、別スレッドで実行するようなものである。
これを使うためにさっきのコードを変更する。
まず、refConを定義して、実行のための関数を作る。こんな感じ。
typedef struct RefConStruct { vector *pVectors; vector *pResults; matrix pMatrix; NSInteger cLength; } refConData; void performProduct(void *refCon) { vector *vectors = ((refConData *)refCon)->pVectors; vector *results = ((refConData *)refCon)->pResults; matrix projmat = ((refConData *)refCon)->pMatrix; NSInteger length = ((refConData *)refCon)->cLength; for (int n = 0 ; n < length ; n ++) mvProduct(results + n, projmat, vectors + n); }refConには実行に必要なデータを全部持たせる必要がある。ちなみにかけ算するマトリクスは自動変数なので値のまるコピーにしてある。
そしてこれを使ってさっきのメソッドを書き換えると
- (void)projectiveTransform:(NSInteger)length forVectors:(vector *)vectors toVectors:(vector *)results { matrix projmat; // マトリクスのセットアップ(省略) dispatch_wob_queue_t queue = dispatch_get_global_wob_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); refConData refCon; refCon.pVectors = vectors; refCon.pResults = results; refCon.pMatrix = projmat; refCon.cLength = length; dispatch_wob_async(queue, performProduct, (void *)(&refCon)); }みたいな感じになる。
さっきのブロックを使うGCDに比べると、コールバック関数では呼び出し元の変数が見えなくなるせいで、多くの作業が必要になって非常に煩わしい。しかしこれまでのCではこれが普通だった。pthreadを使うとすると、これと全く同じことをする必要があるし、例えばCore Foundationの関数の多くはコールバックが設定できて、細かな制御が可能になっているけど、それもこれと同じことをしなければいけない。
逆に、この「ブロックを使わないGCD」はpthreadを使うのとそれほど手間は変わらないので、pthreadでいいじゃん、ということになってしまう。つまりこのGCDは実質的に単なるスレッドプールライブラリと同じで、それほどありがたみはない、ということである。
2013-05-30 22:06
nice!(0)
コメント(0)
トラックバック(0)
コメント 0