pthread の condition

SKK 辞書の初期化を非同期化して検索のテストしているのですが、辞書のロードが完了する前に検索が走る現象がちらほら出てきました。検索が空振りするのでテストも失敗します。対策が必要です。

検索時にコンテナが空かどうか確認するのはどうだろう。できれば、ロードが完了するまでそのまま待っていて欲しい。でも無限に待たれても困るので、せいぜい 3 秒くらいでタイムアウトして欲しい。こうなると、普通の mutex だけで実現するのは難しそうです。

pthread を調べてみると、まさにこういった目的のために condition という仕組みが用意されていました。とはいえ、pthread の API を生のまま叩くのは怖いので、いつものように薄いラッパーを書いてみました。

namespace pthread {
    //...

    class condition {
	mutex mutex_;
	pthread_cond_t handle_;

	condition(const condition&);
	condition& operator=(const condition&);

	void trylock() {
	    assert(EBUSY == pthread_mutex_trylock(&mutex_.handle_) &&
		   "*** You MUST lock the pthread::condition object to avoid race conditions. ***");
	}

    public:
	condition() {
            pthread_cond_init(&handle_, 0);
        }

	~condition() {
            pthread_cond_destroy(&handle_);
        }

	operator mutex&() { return mutex_; }

	void signal() {
	    trylock();

	    pthread_cond_signal(&handle_);
	}

	void broadcast() {
	    trylock();

	    pthread_cond_broadcast(&handle_);
	}

	bool wait() {
	    trylock();

	    return 0 == pthread_cond_wait(&handle_, &mutex_.handle_);
	}

	bool wait(long second, long nano_second = 0) {
	    trylock();

	    timespec abstime;
	    abstime.tv_sec = std::time(0) + second;
	    abstime.tv_nsec = nano_second;

	    return 0 == pthread_cond_timedwait(&handle_, &mutex_.handle_, &abstime);
	}
    };

    // ...
}

pthread のマニュアルを読むと、signal/broadcast/wait/timedwait の事前条件として、条件変数用の mutex がロックされていることを要求しているようです。となると、condition クラスは専用の mutex を保持するのが自然な気がします。とは言え、この mutex は condition クラスの外部からもロックできる必要があるので mutex 参照を返す変換演算子を用意しました。これは無理矢理な気もしますが、まあ、あまり深く考えないことにします。

pthread::condition を使った、典型的な生産者側のコードは以下の通り。

void produce(pthread::condtion& cond, object& obj) {
    pthread::lock scope(cond);

    // ... obj を更新する処理

    cond.signal();
}

対する消費者側のコードはこんな感じになります。

void consume(pthread::condition& cond, object& obj) {
    pthread::lock scope(cond):

    if(obj.empty()) {
        cond.wait();
    }

    // ... obj を消費する処理
}

案外すっきりと書けますね。lock は RAII を使った単純なラッパーです。

    class suspend_cancel {
	void set(int state) {
	    int tmp;
	    pthread_setcancelstate(state, &tmp);
	}

        suspend_cancel(const suspend_cancel&);
        suspend_cancel& operator=(const suspend_cancel&);

    public:
	suspend_cancel() {
	    set(PTHREAD_CANCEL_DISABLE);
	}

	~suspend_cancel() {
	    set(PTHREAD_CANCEL_ENABLE);
	}
    };

    class lock {
	suspend_cancel shield_;
	mutex& target_;

	lock();
	lock(const lock&);
	lock& operator=(const lock&);

    public:
	lock(mutex& target) : target_(target) {
	    pthread_mutex_lock(&target_.handle_);
	}

	~lock() {
	    pthread_mutex_unlock(&target_.handle_);
	}
    };

lock が suspend_cancel クラスを保持しているのは、ロック保持中のスレッドがキャンセルされた場合でも RAII を保証するためです。

Boost.Thread について

最初は boost の Thread ライブラリも調べたのですが、肝心の condition クラスが意外とイマイチな印象です。wait 系のメソッド引数として述語が渡せるのは良いとしても、ScopedLock オブジェクトを渡す仕様は、間違った使い方を招きかねない気がします。素直に condition に mutex を持たせてはいけない理由があるのかなあ。問題を単純化しすぎているのだろうか。

単一の condition オブジェクトに対して複数の ScopedLock オブジェクトを適用することがあるか。いや、これは condition の排他制御にならないから NG ですね。複数の condition オブジェクトに対して、単一の ScopedLock オブジェクトを適用することがあるか。ううむ。アリかもしれない。無駄な待ちが発生しそうだけど。

なんだか釈然としないので、時間を見つけて ACE も見てみるつもりです。