二重化されたオブジェクト(1)
前回に引き続き、BufferedObject のサンプルを紹介します。まずはクラスの使い方をざっと見てみましょう。
primary な値の取得
primary な値はいつでもどのスレッドからも取得可能です。ポインタを介した操作も可能で、値の更新もできます。自由度は高いですが、中途半端な仕様とも言えます。
BufferedObject<int> intbuf; int* p = intbuf.primary(); *p = 100;
secondary な値の取得
secondary な値は、更新可能な時に限り、単一のスレッドだけが取得できます。値が取得できない場合は NULL が返されます。嫌な仕様ですね。
int* p = intbuf.secondary(); if(p) { *p = 100; }
commit と rollback
一旦 secondary が呼び出されると、commit か rollback するまで、secondary な値は取得できなくなります。commit された値は次回 primary 呼び出し時に反映されます。commit と同時ではないところがミソです。rollback は primary への反映を行なわず、再度 secondary な値を取得できる状態にします。しかし、『値』そのものは元に戻りません。これも嫌な仕様ですね。
int* p1 = intbuf.secondary(); if(p1) { *p1 = 100; intbuf.commit(p1); } ... int* p2 = intbuf.secondary(); if(p2 && *p2 != 0) { intbuf.rollback(p2); }
ご覧の通り、commit と rollback には secondary で取得したポインタを指定する必要があります。もしアドレスが異なる場合には、commit と rollback は false を返します。例えば
int* tmp = new int; intbuf.commit(tmp);
などは失敗します。最初に考えた時は、secondary と commit/rollback をペアで使うようにするためのガードのつもりだったのですが、今になってみるといかにも中途半端な感じがしますね。ううむ、なんだか解説すればするほど落ち込んでしまいます。
マルチスレッドのサンプル
さて、クラスの使い方がだいたいわかったところで、マルチスレッドのサンプルを見てみましょう。まずは、メインスレッド側です。
int main(int argc, char** argv) { BufferedObject<int> intbuf; int* p = intbuf.primary(); *p = 1000; p = intbuf.secondary(); *p = 2000; intbuf.rollback(p); pthread_t thread; pthread_create(&thread, NULL, routine, &intbuf); for(int i = 0; i < 16; ++ i) { int* val = intbuf.primary(); std::cerr << "primary =" << *val << std::endl; sleep(1); } pthread_cancel(thread); pthread_join(thread, NULL); return 0; }
primary と secondary の値を初期化した後、いきなり rollback しているのがかなり不気味ですが、ここは見なかったことにします。secondary を書き換えるサブスレッドを走らせた後は、1 秒置きに primary の値を表示するループに入ります。最後にサブスレッドをキャンセルして終了です。
では、secondary を更新するサブスレッド側を見てみましょう。
void* routine(void* param) { BufferedObject<int>* intbuf = static_cast<BufferedObject<int>*>(param); while(1) { pthread_testcancel(); int* val = intbuf->secondary(); if(val) { sleep(2); ++ *val; intbuf->commit(val); std::cerr << "secondary=" << *val << std::endl; } } }
2 秒置きに secondary な値を取得してインクリメントし、commit 後に表示する無限ループになっています。
pthread_testcancel はキャンセルポイントを作るために必要です。pthread の規格 では sleep もキャンセルポイントとして定義されているのですが、実際には pthread_testcancel がないとスレッドをキャンセルできませんでした。詳しく調べていませんが、Mac OS X の pthread 実装は、まだ完全には規格に準拠していないのかもしれません。
さて、プログラムを無事にコンパイルできたら早速実行してみます。
primary =1000 primary =1000 secondary=2001 primary =2001 primary =2001 primary =2001 secondary=1001 primary =1001 primary =1001 primary =1001 secondary=2002 primary =2002 primary =2002 primary =2002 secondary=1002 primary =1002 primary =1002 primary =1002 secondary=2003 primary =2003 primary =2003 secondary=1003
期待通りに動いてるようです。commit の度に primary と secondary の値が切り替わっています。これで、primary へのランダムアクセスを提供しながら、その裏でじっくり secondary を更新可能にする、という要件を満たすことができました。primary と secondary の切り替えショックも、配列の添字を 0 → 1 → 0 → 1 ... と反転しているだけなので、最小限と言えます。
が、しかし、です。これまで見てきた通り、BufferedObject には不自然で妙な仕様がたくさんあって、このままではとても使う気になりません。そこでもうちょっと気持ち良く使えるように、ポインタを排して get/set でインタフェースする実装を考えてみました。続きはまた次回。