フェンス付きデータ転送

問題

順序に一貫性がないメモリーモデルのハードウェアで、メモリーにメッセージを書き込んでいるときに、別のプロセッサーがそのメッセージを読み取る。

コンテキスト

この問題は、同期されていない複数のスレッドがメモリーの同じ場所に同時にアクセスしたとき、または読み取りと書き込みを使用して同期を行う際にのみ発生します。高レベルの同期には、不必要な順序変更を防ぐメカニズムが含まれています。

最近のハードウェアやコンパイラーは、スレッドの操作の順序を保存してメモリー操作の順序を変更することができますが、これはほかのスレッドではなくそのスレッドの観点から行われます。シリアルコードでは、次のコードに示すように、メッセージを書き込んでから準備ができたことを通知します。

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 つです。

  1. Message が書き込まれるまで Ready は true になりません。

  2. Ready が true になるまで Message は読み取れません。

これらの仮定は単一プロセッサー・システムではもちろん当てはまります。しかし、マルチプロセッサー・システムでは当てはまりません。ハードウェアやコンパイラーで順序変更を行うと、送信者の書き込みが順序どおりに行われない (条件 a に当てはまらなくなる)、あるいは受信者の読み取りが順序どおりに行われない (条件 b に当てはまらなくなる) ことがあります。

影響

関連項目

ソリューション

メッセージの準備ができたことを知らせるフラグを bool から tbb::atomic<bool> に変更します。以前のサンプルを修正したサンプル (変更点を太字で表示) を次に示します。

tbb::atomic<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 を返す
}

tbb::atomic への書き込みには release (解放) セマンティクスが用いられ、書き込みを解放する前にその前の書き込みがすべて行われます。値 tbb::atomic からの読み取りには acquire (取得) セマンティクスが用いられ、読み取りを取得した後にその後の読み取りがすべて行われます。tbb::atomic を実装することで、コンパイラーとハードウェアの両方がこれらの順序条件を確認することが保証されます。

バリエーション

高レベルの同期には通常、必要な acquirerelease フェンスが含まれています。例えば、mutex は通常、ロックの取得に acquire セマンティクス、ロックの解放に release セマンティクスを用いて実装されます。このため、mutex でロックを取得するスレッドは常に、その mutex のロックを解放する前に別のスレッドによって行われるすべてのメモリー書き込みを確認します。

誤ったソリューション

よく提案される誤ったソリューションについて、なぜそのソリューションが間違っているか理解しておくと良いでしょう。

よくある誤りは、volatile キーワードを使用してフラグを宣言することで問題が解決すると仮定することです。volatile キーワードは直ちに書き込みを行うように制御しますが、一般にほかのメモリー操作に関連してその書き込みの順序には見た目上の効果はありません。この規則の例外は、インテル® Itanium® プロセッサー・ファミリーのプロセッサーです。これらのプロセッサーは、規則により acquire セマンティクスを volatile の読み取り、release セマンティクスを volatile の書き込みに割り当てています。

別の誤りは、条件をテストする前にその条件によって実行されるコードがないと仮定することです。コンパイラーやハードウェアによっては、条件の前にその条件付きコードを投機的に実行する可能性があります。

同様に、プロセッサーがポインターを読む前にポインターのターゲットを読むことができないと仮定することも誤りです。最近のプロセッサーはメインメモリーから個々の値を読むのではなく、キャッシュラインを読みます。ポインターを読む前に、ポインターのターゲットがすでに読み込まれ、キャッシュラインにある場合、プロセッサーがポインターのターゲットを先に読むことは起こりえます。

関連情報