CFRunLoopSourceの使い方その9 [考え中の問題]
前回作ったCFRunLoopSourceのCラッパを実際に動かしてみる。Core Foundation ToolのXCodeプロジェクトを作って、さっきのrunLoopKickerのコードを読み込む。
threadPrintという関数は、どのスレッドで呼ばれたかと言う情報を出力する関数で
作業スレッドであるworkingTherad関数は
コールバックは
わかりにくいので絵に書くと図-1のようになる。 メインスレッドから作業スレッドを作って、作業スレッドの方ではrunLoopKickerを作ってCFRunLoopをまわす。メインスレッドから1秒ごとに3回実行指令が作業スレッドに伝えられる。
もちろん作業スレッド側からメインスレッドでコールバックを実行させるようにも書けるが、そのときはメインスレッド上でもCFRunLoopを起動する必要がある。このテストコードのままでは作業スレッドにはCFRunLoopはできるけど、メインスレッド側には作られないはずである。
2、4、6行目でメインスレッドから実行指令が出て、そのあとに作業スレッドでコールバック関数が呼ばれている。8行目でremaining変数がfalseに書き換えられて作業スレッドのCFRunLoopが終わっている。9行目で呼ばれた回数が3回であることを出力している。
特に、CFRunLoopSourceの使い方を確認してCocoa感覚の簡単なCラッパを作ってテスト動作させてみた。
pthread単独ではやるべき仕事が発生するたびにスレッドを起こして、終わればスレッドを破棄するというやりかたが一番簡単であるが、その場合小さな仕事がたくさんあるときには、スレッド生成のオーバーヘッドが大きくなってしまう。
CFRunLoopSourceを使えば汎用の作業スレッドを作って、やるべき仕事が発生した時点でその作業スレッドに仕事をさせる、といったようなことが簡単にできる。また、CFRunLoopは無駄なポーリングは極力しないようになっているらしいので、仕事がないときは作業スレッドの負荷をあげないようにできるはずである。
今回、スレッドに関してまとめたのは、実はマルチコアを使い倒すのではなく、machスレッドの調整が必要になったためだった。MacOS Xにはマルチコアの性能を発揮させるためにいくつかの拡張が行われていて、こんな面倒なことをする必要はあまりない。
これらは難しくてデバグが困難なマルチスレッドプログラミングをプログラマから隠蔽して、より簡単にマルチコアを利用できるようにするためのもので、Appleは今後マルチコア化がさらに進んだときにもアプリケーションプログラマの力量がマシンの性能を律速させないようにするための準備を今からしているということだろう。OSや言語を自分たちだけでコントロールできるAppleならではのフットワークの軽さである。
今回見たCFRunLoopSourceを使った方法はいずれ古い技術になっていくだろう。すでに複数のスレッドを対称に使うためであればNSOperationやGCDを使えばこの程度の苦労さえなくなってしまう。
今回なぜCFRunLoopSourceを使ったか、というとひとつのスレッドをリアルタイムスレッドにしてそれに小さなレイテンシが要求される作業をやらせて、他のスレッドでその作業の結果を後からゆっくり利用する、ということがやりたかったからだった。このようなプログラミングは今のところmachスレッドのthread_policy_set()を呼ぶしかなく、Cocoaのレベルでは不可能な微調整である。
しかしそういったクリティカルなプログラミングも、GCDのようなAPIの拡張とハードそのものの性能向上によっていずれはレガシーな技術になるだろう。過渡的な時代特有の技術と言える。いつも過渡的な時代だったような気もするけど。
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のようになる。 メインスレッドから作業スレッドを作って、作業スレッドの方では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〜)
これらは難しくてデバグが困難なマルチスレッドプログラミングをプログラマから隠蔽して、より簡単にマルチコアを利用できるようにするためのもので、Appleは今後マルチコア化がさらに進んだときにもアプリケーションプログラマの力量がマシンの性能を律速させないようにするための準備を今からしているということだろう。OSや言語を自分たちだけでコントロールできるAppleならではのフットワークの軽さである。
今回見たCFRunLoopSourceを使った方法はいずれ古い技術になっていくだろう。すでに複数のスレッドを対称に使うためであればNSOperationやGCDを使えばこの程度の苦労さえなくなってしまう。
今回なぜCFRunLoopSourceを使ったか、というとひとつのスレッドをリアルタイムスレッドにしてそれに小さなレイテンシが要求される作業をやらせて、他のスレッドでその作業の結果を後からゆっくり利用する、ということがやりたかったからだった。このようなプログラミングは今のところmachスレッドのthread_policy_set()を呼ぶしかなく、Cocoaのレベルでは不可能な微調整である。
しかしそういったクリティカルなプログラミングも、GCDのようなAPIの拡張とハードそのものの性能向上によっていずれはレガシーな技術になるだろう。過渡的な時代特有の技術と言える。いつも過渡的な時代だったような気もするけど。
2011-02-16 22:32
nice!(0)
コメント(0)
トラックバック(0)
コメント 0