『同期』ってなに?
マルチスレッドによる並列化の大きな目的の1つは「速くするため」です。原理的には極めて単純、「みんなで手分けして仕事すれば早く片付くんじゃね?」そのとおり。わかりやすぅい♪ ただし、これには「各人が他を気にせず(同時に)仕事ができるならば」という重要な条件があります。
例えば、カレーを作るとしましょう。「オレは玉ネギ刻むからお前はそいつを炒めろ」なんて仕事の割り振りだとちっとも速くなりません。オレが玉ネギ刻み終えるまでオマエは鍋を片手にじっと待ってるしかありませんからね。
「オレは玉ネギ刻むから、オマエはジャガイモの皮むき」ならば同時に仕事ができるから速くなりそうですね。ただし包丁が2本あればね。包丁が1本しかないのなら、1人が使っている間もう1人は包丁が空くまで待ってなくてはなりません(待ってる間に他の仕事ができるなら少しは速くなるかも)。
このように各人がそれぞれ独立して仕事ができるなら、人数に比例して速く(あるいは数多く)こなせるでしょう。しかしながら数人が協力して仕事をするとなれば多くの場合待ち合わせが必要となります。きざみ玉ネギができるまで鍋を片手に待ってるとか、玉ネギの皮むきつつ包丁が空くのを持ってるとか。
大事なのは「ちゃんと待つ」ことです。玉ネギみじん切りの完了を待たずに鍋に放り込んだり、玉ネギ刻んでる最中の包丁を奪い取ったりするとデキソコナイのカレーになってしまいます。ちゃんと待つ/待たせること、それによって使用中のリソースを奪わない/奪われないこと、このためのからくりが同期であり、並列プログラミングのキモのひとつとなります。前述のjoin()も同期のひとつです。スレッドが終了するのを待っているのですから。
もちろん同期を必要としないのが理想です。しかし、1つの仕事を多数で行うのに同期が避けられないのなら、
- できるだけ少ない回数
- できるだけ短い時間
に抑えることが肝要です。同期とは何車線もある高速道路のあちこちに一車線の区間を設けるようなもので、文字通りボトルネックとなります。同期を忘れて失敗する典型例を示します。
#include <tbb/tbb.h>
#include <iostream>
using namespace std;
int main() {
const int N = 100000;
int pvalue = 0; // 入金総額
int nvalue = 0; // 払戻総額
int value = 0; // 預金残高
tbb::tick_count cnt = tbb::tick_count::now();
// 入金スレッド
tbb::tbb_thread deposit([&]() {
for ( int i = 0; i < N; ++i ) {
pvalue += i; value += i;
}
});
// 払戻スレッド
tbb::tbb_thread withdraw([&]() {
for ( int i = 0; i < N; ++i ) {
nvalue -= i; value -= i;
}
});
deposit.join();
withdraw.join();
cout << (tbb::tick_count::now() - cnt).seconds() << "[sec] ";
cout << pvalue << nvalue << '=' << value << endl;
}

1つの当座預金口座(残高0)に対する入金と払出を、それぞれ別のスレッドで行っています。入金・払出の総額は同じになってるコードですから、両スレッドが処理を終えたとき残高は初期値の0になっているはず。ところが実行結果は0になっていませんし、実行のたびに結果の異なる厄介な現象が起こっています。原因は変数valueに対し複数のスレッドが同時に読み書きしていることにあります。value += iは、
- valueを読み
- iを加えて
- 結果をvalueに書き戻す
の3つのステップで実行されます。このステップの途中で他方のスレッドが割り込むことで結果が狂います。(単一スレッドでは起こりえない)マルチスレッドで起こりうる困った現象の代表格「データレース」(data race:データの競合)です。
