â– 

Boostに以前からread-writeロックは実装されていたようですがバグがあったとかで最近の物ではupgrade_lock, upgrade_to_unique_lockにさし変わっています。

ただのロックと比べてパフォーマンスが出やすい上に素性の良い設計だと思うので紹介してみようと思います。

read lock

read-lockをする場合はshared_mutexを引数にshared_lockをかけてやればいいです。

#include <boost/thread.hpp>
using namespace boost;
shared_mutex mutex;
void reader(){
  shared_lock<shared_mutex> read_lock(mutex); // ここでロック!
  // クリティカルセクション
}

スコープを外れると同時にshared_lockのデストラクタでアンロックされます。

write lock

write-lockする場合はshared_mutexに対して upgrade_lock → upgrade_to_unique_lockの順で取得してやる必要があります

#include <boost/thread.hpp>
using namespace boost;
shared_mutex mutex;
void writer(){
  upgrade_lock<shared_mutex> up_lock(mutex);
  upgrade_to_unique_lock write_lock(up_lock); // ここでロック!
  // クリティカルセクション
}

upgrade_lockというのは「同時に一つしか取れない特別なread lock」と考える事ができます。
read lockが既に取られているかどうかに関わらずupgrade_lockはただ一つのスレッドだけが保持可能です。これによりwriter同士の衝突ポイントをreaderとは別の段階で解決する事ができ、read lock→write lockの途切れない昇格が可能となります。*1
同時に一つのスレッドからしか取れないread lockですが、その他のshared_lockを排他する事無くreaderを邪魔しません。

何のためにあるのかというと

#include <boost/thread.hpp>
using namespace boost;
shared_mutex mutex;
void hoge(){
  upgrade_lock<shared_mutex> up_lock(mutex);
  // 書き換える必要があるかどうか調べる
  if(/* 書き換える必要があるなら */){
     upgrade_to_unique_lock write_lock(up_lock); // ここでロック!
     // readerを全て追い出した後のwrite処理
  }else {
     // こっちの処理はreaderを排他せずに行える
  }
}

のように書く事でread_lockと共存可能な利点を生かしスループットの向上が期待できます。

もし教科書的なread writeロックを用いて同様の目的を達成するなら

shared_mutex mutex;
void hoge(){
  read_lock rlock(mutex); // read lockを獲得
  // 書き換える必要があるかどうか調べる
  if(/* 書き換える必要があるなら */){
     rlock.unlock(); // デッドロックを避けるためにread lockを開放する必要がある

     write_lock wlock(mutex); // write lockを獲得
     // 書き換える必要があるかどうか調べる(2度目!
     if(/* 書き換える必要があるなら */){
        // やっと本命の処理
     }
  }
}

このようにread lockとwrite lock確保後にそれぞれチェックする書き方になります、書き換える必要性をチェックする処理が重い場合にはパフォーマンス低下を招きます。
2度チェックするのを避けるには、書き換える必要の無いときにもreaderを全て排斥するwrite lockを獲得するしかありません*2

upgrade_lock達の関係はすこしややこしいので
https://gist.github.com/22c650c292e94631bb84
このようなコードを書いて動作を試してみると良いかも知れません。

「矢印元のロックが確保されている状態で矢印先のロックを新規に確保できるか」を図に表すとこんな感じです。

*1:普通のread writeロックを途切れ無く昇格可能にすると、複数のreaderが昇格する時にお互いのread lockを開放待ちする事になるためデッドロックします

*2:それ以外ではデッドロックした場合の交渉処理を付けた昇格可能ロックにするぐらいしか無さそうな…

実用例

「スレッド間で共有する変数のアクセス権制御を C++ コンパイラで強制する方法」
http://developer.cybozu.co.jp/kazuho/2009/06/c-c79a.html

をupgrade_lockを利用して実装してみます。
これはmutexと保護対象オブジェクトを密結合させたオブジェクトを作る事で、ロック無しでアクセスする危険なコードを禁止することを目的としています。read lockを確保した場合にはconstでしか値を取れないためうっかり書き換える心配もありません。

ヘッダ

// rwsync.hpp
#include <boost/thread.hpp>
#include <boost/noncopyable.hpp>

template <typename T>
class rwsync : boost::noncopyable{
	T m_obj;
	typedef boost::shared_mutex smutex;
	smutex lock;
	friend class read_ref;
	friend class upgrade_ref;
	friend class write_ref;
public:
	class read_ref : public boost::noncopyable{ // read lock
		boost::shared_lock<smutex> m_lock;
		const rwsync<T>* const m_ref;
	public:
		read_ref(rwsync& mutex):m_lock(mutex.lock),m_ref(&mutex){}
		const T& operator*() const { return m_ref->m_obj; }
		const T* operator->() const { return &operator*(); }
	};
	class write_ref;
	class upgrade_ref : public boost::noncopyable{ // upgrade lock
		friend class write_ref;
		boost::upgrade_lock<smutex> m_lock;
		rwsync<T>* const m_ref;
	public:
		upgrade_ref(rwsync& mutex):m_lock(mutex.lock),m_ref(&mutex){}
		const T& operator*() const { return m_ref->m_obj; }
		const T* operator->() const { return &operator*(); }
	};
	class write_ref : public boost::noncopyable{ // unique lock
		boost::upgrade_to_unique_lock<smutex> m_lock;
		rwsync<T>* const m_ref;
	public:
		write_ref(upgrade_ref& lock):m_lock(lock.m_lock),m_ref(lock.m_ref){}
		T& operator*() { return m_ref->m_obj; }
		T* operator->() { return &operator*(); }
		const T& operator*() const { return m_ref->m_obj; }
		const T* operator->() const { return &operator*(); }
	};
};

このようなヘッダを用意しておくことで

// 使用例
#include "rwsync.hpp"

rwsync<std::vector<int> > sync_vector; // 宣言はこんなに簡単!

bool reader(){
  rwsync<std::vector<int> >::read_ref vector_ref(sync_vector); // このようにロックを取ったオブジェクト経由でしかアクセスできない
  for(std::vector<int>::const_iterator iter = vector_ref->begin(); iter != vector_ref->end(); ++iter){
    // 何か処理
  }
}
bool writer(){
  rwsync<std::vector<int> >::upgrade_ref vector_up_ref(sync_vector);
  if(vector_up_ref->front == 10){ // 配列の先頭が10なら
    rwsync<std::vector<int> >::write_ref vector_ref(vector_up_ref);
    vector_ref->push_back(20);
  }
}

のように書けます
rwsyncから何らかのrefを通さない限りロックされているオブジェクトには触れられません。
また、read_refやupgrade_refからはconst付きのポインタ/参照しか得られないためオブジェクトに不用意に触れそうになったらコンパイラがエラーを吐いてくれます。

それを使ってハッシュマップを保護してみた例が以下に続きます。
unordered_mapでmemcached風のセマンティクスを実装します。

続きを読む