メンテナンスフリーな辞書

やっぱり SKK-SJIYO.L は自動的に更新したいですよね。それも、ログアウトやOS の再起動なしに、ユーザーが気付かないうちに、こっそり更新しちゃいたいですよね。

つまり、メンテナンスフリーな辞書です。基本的な要件をまとめてみましょう。

  • 辞書は『常に』検索可能であること
  • 辞書の更新中でもユーザーへのレスポンスは極力低下させないこと

こんなもんですかね。単純に考えると、辞書を二重化することになります。一つは常に検索可能な辞書で、もう一つは更新可能な辞書。で、更新が完了したら二つの辞書をスパっと切り替えてしまえばいいわけです。多少メモリを余分に使いますが、実装はシンプルになります。

問題は、ユーザーが漢字変換(=辞書の検索)をするタイミングがランダムで、事前に予想できないことです。あたりまえのことですが、「このユーザーはしばらくは漢字変換をしないはずだから、今のうちに辞書を更新してしまえ」という戦略は取れません。また、辞書の更新中に変換速度が低下してもいけません。とすると、マルチスレッドで実装するのが自然ということになります。辞書の更新は、メインスレッドとは独立した別のスレッドの仕事になるわけですね。

また、二つの辞書を切り替える時の「ショック」は限りなく小さいほうがいいわけで、ここでも何かうまい『仕掛け』が必要になりそうです。ベタにコードを書いてもいいのですが、せっかくなので(何が?)ここはいい感じのクラスを考えてみることにします。

まずは、マルチスレッドに不可欠な排他機構を提供するコンパクトなクラスを用意します。

class Mutex {
    friend class Guard;
    pthread_mutex_t mutex_;
    void lock() {pthread_mutex_lock(&mutex_);}
    void unlock() {pthread_mutex_unlock(&mutex_);}
public:
    Mutex() { pthread_mutex_init(&mutex_, NULL); }
    ~Mutex() { pthread_mutex_destroy(&mutex_); }
};

class Guard {
    Mutex& mtx_;
public:
    Guard(Mutex& ref) : mtx_(ref) { mtx_.lock(); }
    ~Guard() { mtx_.unlock(); }
};

広く使われるライブラリには向きませんが、今回の用途にはこの程度で十分です。Mutex と Guard が公開しているのはコンストラクタとデストラクタだけですが、二つのクラスは親密な仲なのでイチャイチャできます。基本的な足回りはこれで OK。
次に『二重化されたオブジェクト』を実現するクラスを考えてみます。仕様はこんな感じでしょうか。

  • primary な値はいつでも取得可能
  • secondary な値は更新可能な時のみ取得可能
  • secondary な値の所有者には、commit か rollback の責任が発生する
  • secondary な値を commit すると、primary と secondary が切り替わる
  • secondary な値を rollback すると、secondary が更新可能な状態に戻る

ううむ、ちょっとややこしいですね。イメージ的には OracleRBS みたいなものです。あるいは、画面描画で良く使われる『ダブルバッファ』のテクニックにもちょっと似ているところがあります。

なにはともあれ、コードを見てみましょう。

template <typename T>
class BufferedObject {
    enum { OPEN, LOCK, CLOSE };
    int idx_;
    int secondaryState_;
    T obj_[2];
    Mutex mutex_;

    bool changeState(int newState, T* secondaryObject) {
        Guard on(mutex_);
        if(secondaryState_ == LOCK && secondaryObject == &obj_[!idx_]) {
            secondaryState_ = newState;
        }
        return (secondaryState_ == newState);
    }

public:
    BufferedObject() : idx_(0), secondaryState_(OPEN) {}
    T* primary() {
        Guard on(mutex_);
        if(secondaryState_ == CLOSE) {
            secondaryState_ = OPEN;
            idx_ = !idx_;
        }
        return &obj_[idx_];
    }
    T* secondary() {
        Guard on(mutex_);
        if(secondaryState_ == OPEN) {
            secondaryState_ = LOCK;
            return &obj_[!idx_];
        }
        return NULL;
    }
    bool commit(T* secondaryObject) {
        return changeState(CLOSE, secondaryObject);
    }
    bool rollback(T* secondaryObject) {
        return changeState(OPEN, secondaryObject);
    }
};

はい、こんなもんです。驚きがあるわけでも、目新しい何かがあるわけでもありませんです。ところどころ

Guard on(mutex_);

となっているところが先程のクラスを使った排他制御ですね。スコープを外れると Guard のデストラクタが呼び出されて、自動的にロックも外れます。前述した仕様もいちおう盛り込んであります(ま、逆起こしでもありますが)。

ざっとこんな感じですが、正直な言ってこの BufferedObject クラスは自分でもいまいちしっくりしてません。名前も変だし、どこか間抜けな感じがするんですよね。rollback も不完全だし、もうちょっとシンプルになりそうな気もするんですが、具体的にどこを直せばいいか、まだ見えてない段階です。そのうち良くなるかもしれませんが。

ここで実際のサンプルも紹介しようと思ったのですが、ちょっと量が多くなってしまったので、また次回にしたいと思います。