スレッド セーフについての推論は難しい場合があり、私は C++11 メモリ モデルの専門家ではありません。幸いなことに、あなたの例は非常に単純です。コンストラクターが関係ないので、例を書き直します。
簡単な例
質問:次のコードは正しいですか? または、実行によって未定義の動作が発生する可能性がありますか?
// Legal transfer of pointer to int without data race.
// The receive function blocks until send is called.
void send(int*);
int* receive();
// --- thread A ---
/* A1 */ int* pointer = receive();
/* A2 */ int answer = *pointer;
// --- thread B ---
int answer;
/* B1 */ answer = 42;
/* B2 */ send(&answer);
// wait forever
回答:のメモリ ロケーションでデータ競合が発生している可能性がありanswer
、実行すると未定義の動作が発生します。詳細については、以下を参照してください。
データ転送の実装
もちろん、答えは関数send
との可能な実装と合法的な実装に依存しreceive
ます。次のデータ競合のない実装を使用します。アトミック変数は 1 つだけ使用され、すべてのメモリ操作は を使用することに注意してくださいstd::memory_order_relaxed
。基本的に、これは、これらの関数がメモリの並べ替えを制限しないことを意味します。
std::atomic<int*> transfer{nullptr};
void send(int* pointer) {
transfer.store(pointer, std::memory_order_relaxed);
}
int* receive() {
while (transfer.load(std::memory_order_relaxed) == nullptr) { }
return transfer.load(std::memory_order_relaxed);
}
メモリ操作の順序
マルチコア システムでは、スレッドは他のスレッドとは異なる順序でメモリの変更を確認できます。さらに、コンパイラと CPU の両方が、効率を高めるために 1 つのスレッド内でメモリ操作の順序を変更することがあります。これは常に行われます。のアトミック操作はstd::memory_order_relaxed
同期に参加せず、順序付けも行いません。
上記の例では、コンパイラはスレッド B の操作を並べ替えて、B1 の前に B2 を実行することができます。これは、並べ替えがスレッド自体に影響を与えないためです。
// --- valid execution of operations in thread B ---
int answer;
/* B2 */ send(&answer);
/* B1 */ answer = 42;
// wait forever
データ競争
C++11 はデータ競合を次のように定義します (N3290 C++11 ドラフト):そのようなデータ競合は、未定義の動作を引き起こします。」また、「前に起こる」という用語は、同じドキュメントの前半で定義されています。
上記の例では、B1 と A2 は競合する非アトミック操作であり、どちらも先に発生しません。前のセクションで示したように、両方が同時に発生する可能性があることは明らかです。
それが C++11 で重要な唯一のことです。対照的に、Java メモリ モデルは、データ競合が発生した場合の動作を定義しようとしており、合理的な仕様を考え出すのにほぼ 10 年かかりました。C++11 は同じ過ちを犯しませんでした。
さらに詳しい情報
これらの基本があまり知られていないことに少し驚いています。決定的な情報源は、C++11 標準のマルチスレッド実行とデータ競合のセクションです。ただ、仕様がわかりにくい。
Hans Boehm の講演から始めるのが良いでしょう。たとえば、オンライン ビデオとして入手できます。
他にも多くの優れたリソースがありますが、他の場所で言及しました。