C++/Boostでオブジェクトへのアクセスを同期する方法メモ

C++で、複数のスレッドから1つのオブジェクトにアクセスするときに同期をとる方法いろいろ。

1. atomicを使う

使える型は限られるが、お手軽。

std::atomic<int> x(0);

boost::thread_group threads;
for (int i = 0; i < 4; ++i)
{
    threads.create_thread([&] { for (int i = 0; i < 1000000; ++i) ++x; });
}
threads.join_all();

std::cout << x << std::endl; // 4000000

これはatomic<T>を使う例。
アトミックな操作を順に実行するだけのコードはアトミックでないことに注意。

2. クラスの中でロックする

class integer
{
public:
    int get_value() const
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        return m_value;
    }

    void set_value(int value)
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        m_value = value;
    }

private:
    mutable std::mutex m_mutex;
    int m_value;
};

メンバ関数内でロックをかける。内部ロックと呼ぶことにする。Javaでいうところのsynchronizedメソッドか。
こちらも、単に"synchronizedメンバ関数"の呼び出しを連ねただけのコードはアトミックでないことに注意する必要がある。

2.1. 内部ロック:read-lock/write-lockを使う
class integer
{
public:
    int get_value() const
    {
        boost::shared_lock<boost::shared_mutex> read_lock(m_mutex);
        return m_value;
    }

    void set_value(int value)
    {
        boost::upgrade_lock<boost::shared_mutex> upgrade_lock(m_mutex);
        if (m_value != value)
        {
            boost::upgrade_to_unique_lock<boost::shared_mutex> write_lock(upgrade_lock);
            m_value = value;
        }
    }

private:
    mutable boost::shared_mutex m_mutex;
    int m_value;
};

場合によっては、read-lock/write-lockを使うと効率が上がる…かもしれない(この例はてきとー)。
難しい。

3. クラスの外からロックする

class A
{
public:
    using mutex_type = std::mutex;

    mutex_type &get_mutex() const
    {
        return m_mutex;
    }

    void f()
    {
        // ...
    }

    void g()
    {
        // ...
    }

private:
    mutable mutex_type m_mutex;
};

void fg(A &a)
{
    // 外からロックする。
    std::lock_guard<A::mutex_type> lock(a.get_mutex());

    // 以降の操作全体がアトミックに行われる。
    a.f();
    a.g();
}

これを外部ロックと呼ぶことにする。
しかし、外部ロックの作成を忘れた場合は全く同期されないアクセスが行われてしまう。
それはよろしくないので、内部ロックを併用する。

3.1. 外部ロックと内部ロックを組み合わせる

class A
{
public:
    using mutex_type = std::recursive_mutex;

    mutex_type &get_mutex() const
    {
        return m_mutex;
    }

    void f()
    {
        std::lock_guard<mutex_type> lock(m_mutex);
        // ...
    }

    void g()
    {
        std::lock_guard<mutex_type> lock(m_mutex);
        // ...
    }

private:
    mutable mutex_type m_mutex;
};

void f(A &a)
{
    a.f();
}

void fg(A &a)
{
    std::lock_guard<A::mutex_type> lock(a.get_mutex());
    a.f();
    a.g();
}

基本は内部ロックを使い、複数の操作を行う際は外部ロックを使ってブロック全体をアトミックにする。
同一スレッドから2重にロックする場合があるため、recursive_mutexを使う必要がある。

3.1.1. 外部ロックと内部ロックを組み合わせる:クラスをLockableにする

boost::(basic|timed|shared)_lockable_adapterを使うと楽。

class A
  : public boost::basic_lockable_adapter<boost::recursive_mutex>
{
public:
    void f()
    {
        boost::lock_guard<A> lock(*this);
        // ...
    }

    void g()
    {
        boost::lock_guard<A> lock(*this);
        // ...
    }
};

void fg(A &a)
{
    boost::lock_guard<A> lock(a);
    a.f();
    a.g();
}

単に見た目の違いだが、割と直感的で良いかもしれない。
いつも思うが、adapter/adaptorはBoost内で統一してほしい。

3.2. boost::externally_locked
class A
{
public:
    void f()
    {
        // ...
    }

    void g()
    {
        // ...
    }
};

class B
{
public:
    void a_fg()
    {
        boost::strict_lock<boost::mutex> lock(m_mutex);
        auto &a = m_a.get(lock); // 内部のAオブジェクトを取得する
        a.f();
        a.g();
    }

private:
    boost::mutex m_mutex;
    // 内部にAオブジェクトを保有している
    boost::externally_locked<A, boost::mutex> m_a { m_mutex };
};

ロックのlvalueをexternally_locked::getに渡さないと内部のオブジェクトがとれないため、必然的にローカル変数lockを書くことになり、同期ブロックが導入される。
インターフェースが自然に実装を規定している。良い。

3.3. ロックを内包するスマートポインタ

C++11 std::unique_lock の用例「ロック付きスマートポインタ」 - flatlineの日記とか。
どうせならポインタを介さないとアクセスできないようにする。
下のコードで、sync<T>はTのオブジェクトを保有し、sync<T>::lock()でロックされたTへのポインタを返す。

template <class T>
struct sync
{
public:
    sync() = default;
    explicit sync(T const &obj) : m_obj(obj) {}
    explicit sync(T &&obj) : m_obj(std::move(obj)) {}

    template <class U>
    class basic_pointer
    {
    public:
        using element_type = U;

        basic_pointer(std::mutex &mutex, U &obj)
          : m_lock(mutex), m_ptr(std::addressof(obj))
        {}

        U &operator*() const noexcept { return *m_ptr; }
        U *operator->() const noexcept { return m_ptr; }
        U *get() const noexcept { return m_ptr; }

    private:
        std::unique_lock<std::mutex> m_lock;
        U *m_ptr;
    };

    using pointer = basic_pointer<T>;
    using const_pointer = basic_pointer<T const>;

    pointer lock() { return { m_mutex, m_obj }; }
    const_pointer lock() const { return { m_mutex, m_obj }; }
    const_pointer clock() const { return lock(); }

private:
    std::mutex m_mutex;
    T m_obj;
};

void fg(sync<A> &a)
{
    // pが存在している間、Aはロックされる
    auto const p = a.lock();
    p->f();
    p->g();

    // これを禁止して、ローカル変数pの作成をやんわりと強制することもできる。
    // a.lock()->f();
}

追記:Boostにある(入る予定)らしい。

さらに推し進めて、read-lock/write-lockを内包するポインタを用意し、read-lockポインタからはconstメンバ関数しか呼び出せないようにする、とかできる。
その道の人が書くとこうなるらしい。実用例 - くまメモ

4. おまけ

Boost.Threadはロックのファクトリ関数を提供する。地味に便利。

// Mutexの型を書くのがめんどい
// boost::unique_lock<boost::mutex> lock(mutex);

// Good!
auto const lock = boost::make_unique_lock(mutex);

make_lock_guardもあるが、lock_guardはムーブできないので参照で受けるとかする。

auto const &lock = boost::make_lock_guard(mutex);