フェンス付きデータ転送#
問題
メッセージをメモリーに書き込み、順次一貫性のあるメモリーモデルを持たないハードウェア上で別のプロセッサーにそのメッセージを読み取ります。
コンテキスト
この問題は通常、同期されていないスレッドがメモリー位置に対して同時に動作している場合、または読み取りと書き込みを使用して同期を作成している場合にのみ発生します。高レベルの同期構造には通常、不要な並べ替えを防ぐメカニズムが含まれています。
最新のハードウェアとコンパイラーは、あるスレッドの操作順序をそのスレッドの観点から維持しながら、他のスレッドから見た順序を維持する方法でメモリー操作を並べ替えることができます。シリアルの一般的な慣用句は、次のコードに示すように、メッセージを書き込んで準備完了としてマークすることです。
bool Ready;
std::string Message;
void Send( const std::string& src ){.// スレッド 1 によって実行
Message=src;
Ready = true;
}
bool Receive( std::string& dst ) { // スレッド 2 によって実行
bool result = Ready;
if( result ) dst=Message;
return result; // メッセージを受信した場合は true を返します }コードには主な前提が 2 つあります。
Messageが書き込まれるまでReadyは true になりません。Readyが true になるまでMessageは読み取られません。
これらの仮定は、単一プロセッサーのハードウェアには当然当てはまります。ただし、マルチプロセッサー・ハードウェアでは動作しない可能性があります。ハードウェアまたはコンパイラーによる順序変更により、送信側の書き込みが受信側に順序どおりに表示されなくなったり (つまり条件 a が破られる)、受信側の読み取りが順序どおりに表示されなくなる (つまり条件 b が破られる) 可能性があります。
強制
生の読み取りと書き込みを介して同期を作成します。
解決方法
メッセージの準備が完了したことを示すフラグを bool から std::atomic<bool> に変更します。以下は前の例に変更を加えたものです。
std::atomic<bool> Ready;
std::string Message;
void Send( const std::string& src ) { // スレッド 1 によって実行
Message=src;
Ready.store(true, std::memory_order_release);
}
bool Receive( std::string& dst ) { // スレッド 2 によって実行
bool result = Ready.load(std::memory_order_acquire);
if( result ) dst=Message;
return result; // メッセージを受信した場合は true を返します }std::atomic 値への書き込みに 解放セマンティクスがあり、解放書き込みの前にそれ以前の書き込みがすべて表示されることを意味します。std::atomic 値からの読み取りには取得セマンティクスがあり、これは、取得読み取りの後に後続のすべての読み取りが実行されることを意味します。std::atomic の実装により、コンパイラーとハードウェアの両方がこれらの順序制約を遵守することが保証されます。
バリエーション
高レベルの同期構造には通常、必要な取得フェンスと解放フェンスが含まれます。例えば、ミューテックスは、通常、ロックの取得には取得セマンティクスが使用され、ロックの解放には解放セマンティクスが使用されるように実装されます。したがって、ミューテックスのロックを取得したスレッドは、そのミューテックスのロックを解除する前に、別のスレッドによって行われたメモリー書き込みを常に認識します。
解決策なし
誤った解決策が頻繁に提案されるため、なぜそれが間違っているのか理解する価値があります。
よくある間違いの 1 つは、volatile キーワードを使用してフラグを宣言することで問題が解決すると考えることです。volatile キーワードは、書き込みを即時に実行するよう強制しますが、通常、他のメモリー操作に関して書き込みの順序には影響しません。
もう 1 つの間違いは、条件がテストされる前に条件付きで実行されるコードはないと仮定することです。ただし、コンパイラーまたはハードウェアが、条件コードを条件式より前に推測的に移動する場合があります。
同様に、プロセッサーがポインターを読み取る前に、ポインターのターゲットを読み取ることができないと仮定するのは誤りです。現代のプロセッサーはメインメモリーから個々の値を読み取りません。キャッシュラインを読み取ります。ポインターのターゲットは、ポインターが読み取られる前にすでに読み取られているキャッシュライン内にあることがあり、その結果、プロセッサーがポインターのターゲットを事前に予測して読み取ったように見えます。