二重化されたオブジェクト(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 でインタフェースする実装を考えてみました。続きはまた次回。