reader_writer_mutex
次のサンプルは「そろばん塾の演習」です。そろばん塾には100人の生徒と2人の先生がいます。教室には黒板がひとつあり、
- 先生は100個の数を問題の欄に、その総和を答のマスに書く
- 生徒は黒板の問題を読み100個の数の総和を求めてこたえあわせをする
もちろん先生は他方の先生が出題している間、あるいは生徒のうち1人でも黒板を読んでいる間は黒板を書き換えることができませんし、生徒はいずれかの先生が黒板に書いている間は読むことができません。黒板をmutexでガードしなければならないようです。
#include <tbb/tbb.h>
#include <iostream>
#include <array>
#include <cstdlib>
#include <algorithm>
#include <numeric>
#include <functional>
using namespace std;
// 黒板
struct black_board {
typedef tbb::spin_mutex mutex_type;
mutex_type mtx;
array<int,100> question;
int answer;
};
void run() {
const int T = 2; // 先生の数
const int S = 100; // 生徒の数
const int Q = 1000; // 問題数
tbb::atomic<int> exams; // 出題数
tbb::atomic<int> answers; // 解答数
tbb::atomic<int> wrongs; // 誤答数
bool start = false;
bool stop = false;
// 先生のタスク
auto teachers_task = [&](black_board& b) {
while ( !start ) tbb::this_tbb_thread::yield();
while ( !stop ) {
black_board::mutex_type::scoped_lock lock(b.mtx);
generate(b.question.begin(), b.question.end(), []() { return rand()%100;});
b.answer = accumulate(b.question.begin(), b.question.end(), 0);
++exams;
}
};
// 生徒のタスク
auto students_task = [&](black_board& b) {
array<int,100> notebook;
int expected;
while ( !start ) tbb::this_tbb_thread::yield();
while ( !stop ) {
{
black_board::mutex_type::scoped_lock lock(b.mtx);
copy(b.question.begin(), b.question.end(), notebook.begin());
expected = b.answer;
}
// 総和を求めて
int answer = 0;
for ( unsigned i = 0; i < notebook.size(); ++i ) {
answer += notebook[i];
}
// こたえあわせ
++answers;
if ( answer != expected ) {
++wrongs;
}
}
};
black_board board;
fill(board.question.begin(),board.question.end(),0);
board.answer = 0;
exams = 0;
wrongs = 0;
tbb::tick_count cnt = tbb::tick_count::now();
// T個の先生スレッド
array<tbb::tbb_thread,T> teachers;
for ( int i = 0; i < T; ++ i ) {
teachers[i] = tbb::tbb_thread(teachers_task, ref(board));
}
// S個の生徒スレッド
array<tbb::tbb_thread,S> students;
for ( int i = 0; i < S; ++ i ) {
students[i] = tbb::tbb_thread(students_task, ref(board));
}
start = true; // 開始
while ( exams < Q ) tbb::this_tbb_thread::yield();
stop = true; // 終了
for ( int i = 0; i < T; ++ i ) { teachers[i].join(); }
for ( int i = 0; i < S; ++ i ) { students[i].join(); }
double duration = (tbb::tick_count::now() - cnt).seconds();
cout << " answers/sec: " << answers/duration << '\t';
cout << " wrongs: " << wrongs << endl;
}
int main() {
for ( int i = 0; i < 10; ++i ) {
run();
}
}

ちゃんと動いていますけど...でもちょっと待って。生徒のふるまいがぎこちなくありませんか? 生徒は黒板が書き換えられている間は読んじゃいけません。けども他の生徒が読んでいる間は読んでもかまいません。なのにひとつのmutexから実行権を取得しているので「誰も読み書きしていないとき読める」ことになり、先生はこれでいいけど生徒には制限がキツすぎます。「誰も書いていないなら読める」であって欲しいのです。
tbb::spin_rw_mutexは書き手用と読み手用の2通りの実行権をaquireできます。tbb::spin_rw_mutex::scoped_lockのコンストラクタの第2引数がtrueなら書き手用、falseなら読み手用の実行権でブロックします(省略すれば書き手用)。先のコードでtbb::spin_mutexをtbb::spin_rw_mutexに差し替え、先生に書き手用、生徒に読み手用実行権をaquireさせると、

ずいぶん速くなっています。生徒がほかの生徒が読んでいるかを気にしなくなったからです。
