SSブログ

なぜブロックなのか? - 前編 [プログラミング]

OS XとiOSでマルチスレッドなアプリを作るためにGCD(Grand Central Dispatch、Appleによる日本語pdf)というCから呼べる機能がOS Xでは10.6から追加された。これはdispatchというライブラリとC言語に「ブロックAppleによる日本語pdf)」という仕様を追加して実現されている。GCDは便利なんだけど、Cの文法にいわゆる「クロージャ」というCとは異質な仕様を追加してまでやるべきことなのか?という疑問が湧いた。

「ブロック」について今日ともう1回かけて考えてみる。今日は例を使ってシングルスレッドなコードをGCDとブロックを使ってマルチスレッドにすること、そしてもしGCDがブロックを使わないとしたらどうなるか、ということを考えてみる。

1.0  はじめに

いまやろうとしているポリゴン描画アプリでは、レスポンスが重要なのでマルチスレッドに書きたいと思っている。

OS Xではマルチスレッドなアプリのために
  • NSOperation
  • GCD(Grand Central Dispatch)
が用意されている。NSOperationは10.5から、GCDは10.6から利用可能でCocoa/Objective-Cのレベル(具体的にはObjective-Cのランタイムをリンクしたアプリ)ではNSOperationが使えて、そうでないCのレベルではGCDが使える。これらはiOSでもまったく同じコードが動く。

まず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は実質的に単なるスレッドプールライブラリと同じで、それほどありがたみはない、ということである。
nice!(0)  コメント(0)  トラックバック(0) 

nice! 0

コメント 0

コメントを書く

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

トラックバック 0

献立05/30献立05/31 ブログトップ

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