SSブログ

CFRunLoopSourceの使い方その9 [考え中の問題]

前回作ったCFRunLoopSourceのCラッパを実際に動かしてみる。Core Foundation ToolのXCodeプロジェクトを作って、さっきのrunLoopKickerのコードを読み込む。

4.10  動作確認用のコード

main関数に
#include <CoreFoundation/CoreFoundation.h>
#include <pthread.h>
#include <unistd.h>
#include "RunLoopKicker.h"

static pthread_t    mainThread;
static bool         remaining;

int main (int argc, const char * argv[])
{
    pthread_t       thread;
    runLoopKicker   *kicker;

    mainThread = pthread_self();    //  (1)
    remaining = true;
    threadPrint("start");
    //  (2) create working thread
    pthread_create(&thread, NULL, workingThread, (void *)(&kicker));
    sleep(1);
    threadPrint("kick first"); 
    kickPerform(kicker);            //  (3)
    sleep(1);
    threadPrint("kick second");
    kickPerform(kicker);            //  (4)
    sleep(1);
    threadPrint("kick third");
    kickPerform(kicker);            //  (5)
    sleep(1);
    threadPrint("finish working");
    remaining = false;                //  (6)
    sleep(1);
    threadPrint("exiting");
    return 0;
}
とする。(1)で静的変数のmainThreadを設定している。これはあとで説明する。(2)で作業スレッドを作っている。作業スレッドはworkingThreadという関数が実体で、引数としてrunLoopKickerのオブジェクトの変数の位置を渡している。その後(3)〜(5)の3回kickPerformを呼んでrunLoopKickerを起動している。(6)で静的変数であるremainingをfalseに書き換えている。

threadPrintという関数は、どのスレッドで呼ばれたかと言う情報を出力する関数で
static void threadPrint(char *s)
{
    static int  line = 0;
    bool        isMain = pthread_equal(mainThread, pthread_self());
    printf("%2d %s : %s\n",
           line ++,
           (isMain ? "   main thread" : "working thread"),
           s);
}
静的変数のmainThreadと呼ばれたスレッドとを比較してそれに対応する文字列を返している。静的変数はこのために設定した。CFRunLoopを比較すればこの静的変数はいらなくなるけど、まあこっちの方が簡単だし。

作業スレッドであるworkingTherad関数は
static void *workingThread(void *refCon)
{
    int             count = 0;  //  (7)
    runLoopKicker   **kicker = (runLoopKicker **)(refCon);
    //  (8) create kicker
    *kicker = createRunLoopKicker(callback,  &count, CFRunLoopGetCurrent());
    threadPrint("start run loop");
    while (remaining) //  (9)
        CFRunLoopRunInMode(runLoopKickerMode, 0.1, true);
    char    str[64];
    sprintf(str, "%d times called", count);
    threadPrint(str);
    deallocateRunLoopKicker(*kicker);
    return NULL;
}
というようなもの。呼ばれる回数を数えるcount変数を(7)で初期化している。(8)でrunLoopKickerオブジェクトを作る。コールバック関数はcallbackで、count変数の位置をrefConとして渡している。そして(9)でremainingという変数がfalseになるまで作業スレッドでCFRunLoopをまわしている。

コールバックは
static void callback(void *refCon)
{
    int *count = (int *)refCon;
    threadPrint("callback function called");
    sleep(1);
    (*count) ++;
}
で、count変数を取ってきて1秒待ってからそれをインクリメントするだけ。

わかりにくいので絵に書くと図-1のようになる。
0216fig01.png
メインスレッドから作業スレッドを作って、作業スレッドの方ではrunLoopKickerを作ってCFRunLoopをまわす。メインスレッドから1秒ごとに3回実行指令が作業スレッドに伝えられる。

もちろん作業スレッド側からメインスレッドでコールバックを実行させるようにも書けるが、そのときはメインスレッド上でもCFRunLoopを起動する必要がある。このテストコードのままでは作業スレッドにはCFRunLoopはできるけど、メインスレッド側には作られないはずである。

4.11  テストコードの実行

このテストコードを実行してみる。
run
[Switching to process 22563]
実行中...
 0    main thread : start
 1 working thread : start run loop
 2    main thread : kick first
 3 working thread : callback function called
 4    main thread : kick second
 5 working thread : callback function called
 6    main thread : kick third
 7 working thread : callback function called
 8    main thread : finish working
 9 working thread : 3 times called
10    main thread : exiting

Debugger stopped.
Program exited with status value:0.
0行目でメインスレッドが、1行目で作業スレッドが生成されて作業スレッドではCFRunLoopが起動される。

2、4、6行目でメインスレッドから実行指令が出て、そのあとに作業スレッドでコールバック関数が呼ばれている。8行目でremaining変数がfalseに書き換えられて作業スレッドのCFRunLoopが終わっている。9行目で呼ばれた回数が3回であることを出力している。

5  まとめ

Cocoaではなく、Core Foundationのレベルでマルチスレッドなプログラミングをする方法をおさらいしてみた。

特に、CFRunLoopSourceの使い方を確認してCocoa感覚の簡単なCラッパを作ってテスト動作させてみた。

pthread単独ではやるべき仕事が発生するたびにスレッドを起こして、終わればスレッドを破棄するというやりかたが一番簡単であるが、その場合小さな仕事がたくさんあるときには、スレッド生成のオーバーヘッドが大きくなってしまう。

CFRunLoopSourceを使えば汎用の作業スレッドを作って、やるべき仕事が発生した時点でその作業スレッドに仕事をさせる、といったようなことが簡単にできる。また、CFRunLoopは無駄なポーリングは極力しないようになっているらしいので、仕事がないときは作業スレッドの負荷をあげないようにできるはずである。

今回、スレッドに関してまとめたのは、実はマルチコアを使い倒すのではなく、machスレッドの調整が必要になったためだった。MacOS Xにはマルチコアの性能を発揮させるためにいくつかの拡張が行われていて、こんな面倒なことをする必要はあまりない。
  • NSObjectのperformSelector:onThread:withObject:waitUntilDone:メソッド追加(10.5〜)
  • NSOperationクラス(10.5〜)
  • Grand Central Dispatch(10.6〜)
NSObjectには10.2からメインスレッドでセレクタで指定されたメソッドを実行するメソッドがあったが、それが10.5からはどのスレッドでも指定できるようになった。さらにNSOperationでスレッドを意識せずにマルチスレッド化することができるようになった。そしてさらに10.6からはGCDでシステムワイドなマルチコア性能の最適化ができるような手段が提供されるようになった。しかもGCDはCocoaだけでなくCore Foundationのレベルで利用可能になっている。

これらは難しくてデバグが困難なマルチスレッドプログラミングをプログラマから隠蔽して、より簡単にマルチコアを利用できるようにするためのもので、Appleは今後マルチコア化がさらに進んだときにもアプリケーションプログラマの力量がマシンの性能を律速させないようにするための準備を今からしているということだろう。OSや言語を自分たちだけでコントロールできるAppleならではのフットワークの軽さである。

今回見たCFRunLoopSourceを使った方法はいずれ古い技術になっていくだろう。すでに複数のスレッドを対称に使うためであればNSOperationやGCDを使えばこの程度の苦労さえなくなってしまう。

今回なぜCFRunLoopSourceを使ったか、というとひとつのスレッドをリアルタイムスレッドにしてそれに小さなレイテンシが要求される作業をやらせて、他のスレッドでその作業の結果を後からゆっくり利用する、ということがやりたかったからだった。このようなプログラミングは今のところmachスレッドのthread_policy_set()を呼ぶしかなく、Cocoaのレベルでは不可能な微調整である。

しかしそういったクリティカルなプログラミングも、GCDのようなAPIの拡張とハードそのものの性能向上によっていずれはレガシーな技術になるだろう。過渡的な時代特有の技術と言える。いつも過渡的な時代だったような気もするけど。
nice!(0)  コメント(0)  トラックバック(0) 

nice! 0

コメント 0

コメントを書く

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

トラックバック 0

献立02/16夢を見た ブログトップ

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