これは16日目の Boost Advent Calendar 2011 の参加記事です。
マルチスレッドのプログラムを作る場合、切っても切れないのがmutexです。
同じリソースにアクセスする時はmutexのロックを使って、
複数のスレッドが同時にアクセスするのを制限しないといけません。
例えば、std::cout で文字列を出力するのはスレッドセーフではないので、
複数のスレッドから同時に呼び出すと、出力が混じり合うことがあります。
なので、こんな感じでmutexを使って排他にしなければなりません。
static boost::mutex dispmutex;
void Display(const std::string &fname, const std::string &str){
boost::mutex::scoped_lock lk(dispmutex);
std::cout << fname << ":" << str << std::endl;
}
scoped_lockを使うことで、そのスコープの間だけロックを掛ける、ということができます。
途中でreturnやbreakなどでスコープを抜けた場合もロックを外してくれるので、
mutexのlock/unlockを使うよりは、scoped_lockを使うといいでしょう。
さて、預金管理システムを作ってみました。
残高照会、預け入れ、引き出しができます。
預け入れに上限チェックしていませんが、
私の預金が32bitの上限が超えそうなときまでには考えます。
なお、どの関数も1秒ほどの処理時間がかかるものとします。
class Yokin{
boost::mutex access;
int money;
public:
Human(int money):money(money){}
void Zandaka(){
boost::scoped_lock lk(access);
Display(__FUNCTION__, boost::lexical_cast<std::string>(money));
}
void Azukeire(int h){
boost::scoped_lock lk(access);
money += h;
Display(__FUNCTION__, boost::lexical_cast<std::string>(money));
}
void Hikidashi(int h){
boost::scoped_lock lk(access);
if (h <= money){
money -= h;
}
Display(__FUNCTION__, boost::lexical_cast<std::string>(money));
}
};
これは無駄な部分があるので、高速化してみます。
Zandaka()は値を読み込んで表示しているだけですので、
Azukeire()やHikidashi()が動いていない間なら並列で走らせても問題なさそうです。
そこで、boost::mutexより細かくロックレベルを変えられるのがboost::shared_mutexです。
それを使って先ほどのクラスを書き換えてみます。
class Yokin{
boost::shared_mutex access;
int money;
public:
Human(int money):money(money){}
void Zandaka(){
boost::shared_lock<boost::shared_mutex> read(access);
Display(__FUNCTION__, boost::lexical_cast<std::string>(money));
}
void Azukeire(int h){
boost::unique_lock<boost::shared_mutex> write(access);
money += h;
Display(__FUNCTION__, boost::lexical_cast<std::string>(money));
}
void Hikidashi(int h){
boost::unique_lock<boost::shared_mutex> write(access);
if (h <= money){
money -= h;
}
Display(__FUNCTION__, boost::lexical_cast<std::string>(money));
}
};
値を読むだけならshared_lockを使い並列に動くようにして、
値を書き換える場合はunique_lockを使って排他にします。
これで、残高照会中に別スレッドの残高照会が動くようになって高速化しました。
しかし、もうちょっと高速化の余地があります。
Hikidashi()を見てみると、お金が足りない時はデータを書き換えません。
つまり、お金があるときのみ、unique_lockをすればよいです。
それを踏まえて書き換えてみます。
void Hikidashi(int h){
boost::shared_lock<boost::shared_mutex> shared(access);
if (h <= money){
boost::unique_lock<boost::shared_mutex> unique(access);
money -= h;
}
Display(__FUNCTION__, boost::lexical_cast<std::string>(money));
}
更に高速化!…するどころか、返ってきません。
どうやら、デッドロックしているようです。
お金を減らすとき、unique_lockを通過するには
shared_lockを含め、すべてのロックが外れていることが条件です。
しかし、自分自身がshared_lockをかけています。
void Hikidashi(int h){
boost::shared_lock<boost::shared_mutex> shared(access);
if (h <= money){
shared.unlock();
boost::unique_lock<boost::shared_mutex> unique(access);
money -= h;
}
Display(__FUNCTION__, boost::lexical_cast<std::string>(money));
}
スコープの途中でも、unlock関数を使うことで、ロックを外すことができます。
もう使わない!となったらすぐに開放したほうがいいでしょう。
こうして、デッドロックはなくなりましたが…残高がマイナス??
どうやら、if文を抜けた後、別のスレッドで値を書き換えられたようです。
ロックは一度確保したら使い終わるまで一瞬でも開放してはいけません。
今回はshared_lockのアンロック→unique_lockの間に一瞬ロックが外れていて
その間に別のスレッドが値を書き換えているわけです。
こういう、書き換えるかもしれないときに使うロックがupgrade_lockです。
void Hikidashi(int h){
boost::upgrade_lock<boost::shared_mutex> upgrade(access);
if (h <= money){
boost::upgrade_to_unique_lock<boost::shared_mutex> write(upgrade);
money -= h;
}
Display(__FUNCTION__, boost::lexical_cast<std::string>(money));
}
upgrade_lockはshared_lockとは干渉しないので
引き出し額が足りないときはZandaka()とは並列して動きます。
引き出し額が足りているときは、upgrade_to_unique_lockを使ってunique_lockに切りかえ、
その時にshared_lockが解除されるまで待ちます。
まとめると以下のようになります。
shared_lock |
unique_lockがあるときにブロック |
upgrade_lock |
upgrade_lock, unique_lockがあるときにブロック unique_lockに変更可能 |
unique_lock |
shared_lock, upgade_lock, unique_lockがあるときにブロック |
アクセスが集中するリソースではこれらのロックを適切に使っていきたいです。
以上、Boost Advent Calender 2011 16日目、shared_mutexの紹介でした。
明日の17日目は @egtra さんです。
よろしくお願いします。