C++ 11には、多数の新しい乱数ジェネレータエンジンと分布関数があります。それらはスレッドセーフですか?複数のスレッド間で単一のランダムな分布とエンジンを共有する場合、それは安全であり、それでも乱数を受け取りますか?私が探しているシナリオは、次のようなものです。
void foo() {
std::mt19937_64 engine(static_cast<uint64_t> (system_clock::to_time_t(system_clock::now())));
std::uniform_real_distribution<double> zeroToOne(0.0, 1.0);
#pragma omp parallel for
for (int i = 0; i < 1000; i++) {
double a = zeroToOne(engine);
}
}
openMPまたは
void foo() {
std::mt19937_64 engine(static_cast<uint64_t> (system_clock::to_time_t(system_clock::now())));
std::uniform_real_distribution<double> zeroToOne(0.0, 1.0);
dispatch_apply(1000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t i) {
double a = zeroToOne(engine);
});
}
libdispatchを使用します。
C++ 11標準ライブラリは、広くスレッドセーフです。 PRNGオブジェクトのスレッドセーフ保証はコンテナの場合と同じです。より具体的には、PRNGクラスはすべて疑似-ランダム、つまり、明確な現在の状態に基づいて決定論的なシーケンスを生成します。実際には、含まれている状態(ユーザーにも表示されます)以外のものを覗いたり突いたりする余地はありません。
コンテナを安全に共有するためにロックが必要なのと同じように、PRNGオブジェクトをロックする必要があります。これにより、処理が遅くなり、決定論的ではなくなります。スレッドごとに1つのオブジェクトの方が適しています。
§17.6.5.9[res.on.data.races]:
1このセクションでは、データの競合を防ぐために実装が満たす必要のある要件を指定します(1.10)。特に指定がない限り、すべての標準ライブラリ関数は各要件を満たす必要があります。実装により、以下に指定されている以外の場合にデータの競合が防止される場合があります。
2 C++標準ライブラリ関数は、これを含む関数の引数を介してオブジェクトに直接または間接的にアクセスしない限り、現在のスレッド以外のスレッドからアクセスできるオブジェクト(1.10)に直接または間接的にアクセスしてはなりません。
3 C++標準ライブラリ関数は、これを含む関数のnon-const引数を介してオブジェクトに直接または間接的にアクセスしない限り、現在のスレッド以外のスレッドからアクセスできるオブジェクト(1.10)を直接的または間接的に変更してはなりません。
4 [注:これは、たとえば、スレッド間でオブジェクトを明示的に共有していないプログラムでもデータ競合が発生する可能性があるため、実装が同期なしで静的オブジェクトを内部目的で使用できないことを意味します。 —文末脚注]
5 C++標準ライブラリ関数は、それらのコンテナ要素の仕様で必要な関数を呼び出す場合を除いて、引数またはコンテナ引数の要素を介して間接的にアクセスできるオブジェクトにアクセスしてはなりません。
6標準ライブラリコンテナまたは文字列メンバー関数を呼び出すことによって取得されたイテレータの操作は、基になるコンテナにアクセスできますが、変更することはできません。 [注:特に、イテレーターを無効にするコンテナー操作は、そのコンテナーに関連付けられたイテレーターの操作と競合します。 —エンドノート]
7オブジェクトがユーザーに表示されず、データ競合から保護されている場合、実装はスレッド間で独自の内部オブジェクトを共有する場合があります。
8特に指定のない限り、C++標準ライブラリ関数は、ユーザーに表示される(1.10)効果がある場合、現在のスレッド内でのみすべての操作を実行するものとします。
9 [注:これにより、目に見える副作用がない場合、実装は操作を並列化できます。 —エンドノート]
標準(まあN3242
)乱数生成がレースフリーであることについては言及していないようです(Rand
がそうではないことを除いて)、したがって(何かを逃した場合を除いて)そうではありません。さらに、実際には何も獲得せずに、(少なくとも数値自体の生成と比較して)比較的大きなオーバーヘッドが発生するため、スレッドを保存しても意味がありません。
さらに、スレッドごとに1つではなく、共有乱数ジェネレーターを1つ持つことで、それぞれがわずかに異なる方法で初期化されるという利点は実際にはわかりません(たとえば、別のジェネレーターの結果や現在のスレッドIDから)。結局のところ、とにかく実行するたびに特定のシーケンスを生成するジェネレーターに依存しないでしょう。したがって、コードを次のように書き直します(openmp
の場合、libdispatch
についての手がかりはありません):
void foo() {
#pragma omp parallel
{
//just an example, not sure if that is a good way too seed the generation
//but the principle should be clear
std::mt19937_64 engine((omp_get_thread_num() + 1) * static_cast<uint64_t>(system_clock::to_time_t(system_clock::now())));
std::uniform_real_distribution<double> zeroToOne(0.0, 1.0);
#pragma omp for
for (int i = 0; i < 1000; i++) {
double a = zeroToOne(engine);
}
}
}
ドキュメント はスレッドセーフについて言及していないので、notスレッドセーフ。