atomic
TBBでこのデータレースを解消するには、先のコードのvalue = 0;を
tbb::atomic<int> value; value = 0;
に差し替えるだけで常に正しい結果が得られます。

atomic(アトミック)とは分割不能の意。一連の操作の途中に他が割り込むことを許しません。tbb::atmic<T>は、これに対する以下の操作
- 代入:=
- インクリメント:++
- デクリメント:--
- 加算:+=
- 減算:-=
など(詳細はリファレンスを参照)が行われている間、ほかのスレッドによる操作をブロックして同期をとります。ブロックされている時間は他の同期メカニズムに比べて非常に短く、パフォーマンスの低下を抑えることができます。データレースの防止策はatomicの利用が第1候補となります。
残念ながらatomicで扱えるのはint, longなどの整数型、ポインタ, boolに限られていますし、複数の変数をまとめてatomicにはできません。
atomic<int> x; atomic<long> y; ... ++x; y -= 5;
とかやったとき、++xとy -= 5との間に他のスレッドによる操作が割り込めてしまいます。atomicが使えるシチュエーションはかなり限定的で「使えりゃラッキー」程度かな、と。
mutexとspin_mutex
一連の処理の途中に他のスレッドが割り込むのを抑止すべく、TBBにはmutexが用意されています。mutexとはmutual executionの略、「mutual」は「相互に分け合う」とか「共通の/共有した」あるいは「お互いさま」といった意味で、実行権を司るカギみたいなものです。mutexには大きくふたつ、acquire(取得)とrelease(解放)の動作があります。
- acquire:実行権を手に入れる。誰かが取得済みであれば解放されるまで待つ
- release:実行権を手放す(解放する)
要するに、mutexをacquireすればその時点からreleaseするまでの間実行権をキープし、他スレッドのacquireはreleaseまで待たされます。
当座預金口座のサンプルにmutexを使うと、
#include <tbb/tbb.h>
#include <iostream>
using namespace std;
void run() {
const int N = 1000000;
long long pvalue = 0;
long long nvalue = 0;
long long value = 0;
tbb::mutex mtx;
tbb::tick_count cnt = tbb::tick_count::now();
tbb::tbb_thread add([&]() {
for ( int i = 0; i <= N; ++i ) {
tbb::mutex::scoped_lock lock(mtx);
pvalue += i; value += i;
}});
tbb::tbb_thread sub([&]() {
for ( int i = 0; i <= N; ++i ) {
tbb::mutex::scoped_lock lock(mtx);
nvalue -= i; value -= i;
}});
add.join();
sub.join();
cout << (tbb::tick_count::now() - cnt).seconds() << "[sec]\t";
cout << pvalue << nvalue << '=' << value << endl;
}
int main() {
for ( int i = 0; i < 10; ++i ) {
run();
}
}

tbb::mutex::scoped_lockオブジェクトはそのコンストラクタでaquireし、デストラクタでreleaseしますから、オブジェクトの存在するスコープを他スレッドから守ってくれます。
mutexのバリエーションにspin_mutexがあります。上記コードのtbb::mutexをtbb::spin_mutexに差し替えると、

どうです? ずいぶん速くなっていませんか? この理由は、acquireによる待ち方の違いによるものです。mutexのacquireは実行権が得られないと「使えるようになったら起こしてネ」とOSにお願いし、スレッドが休眠状態に入ります。一方、spin_mutexのaquireは実行権が得られるまでその場で足踏み待機、つまりビジーウェイトします。スレッドの休眠/覚醒状態を切り替えるのにはそれなりの時間的コストがかかるため、起きたまま待ってるspin_mutexの方が速いんです。
とはいえ、仕事もないのに無駄に足踏みして待っているのですから、他のスレッドの実行時間を圧迫します。特にI/Oの絡むような長い時間をspin_mutexでガードするのは禁忌です。aquireからreleaseまでのガード時間が長いとmutexの方が速くなります。どちらを選ぶかは...case-by-caseでしょうか。
