同期とマルチプロセッサの問題
アプリケーションは、シングル プロセッサ システムでのみ有効であるという前提により、マルチプロセッサ システムで実行するときに問題が発生する可能性があります。
スレッドの優先順位
2 つのスレッドを含むプログラムについて考えます。1 つはもう一方のスレッドよりも優先順位の高いプログラムです。 単一プロセッサ システムでは、スケジューラは優先順位の高いスレッドを優先するため、優先順位の高いスレッドは優先順位の低いスレッドに制御を放棄しません。 マルチプロセッサ システムでは、両方のスレッドをそれぞれ独自のプロセッサで同時に実行できます。
競合状態を避けるために、アプリケーションはデータ構造へのアクセスを同期する必要があります。 優先順位の高いスレッドが、優先順位の低いスレッドからの干渉なしに実行されることを前提とするコードは、マルチプロセッサ システムで失敗します。
メモリの順序付け
プロセッサがメモリの場所に書き込むと、パフォーマンスを向上させるために値がキャッシュされます。 同様に、プロセッサは、パフォーマンスを向上させるためにキャッシュからの読み取り要求を満たそうとします。 さらに、プロセッサは、アプリケーションによって要求される前に、メモリから値をフェッチし始めます。 これは、投機的実行の一環として、またはキャッシュ ラインの問題が原因で発生する可能性があります。
CPU キャッシュは、並列でアクセスできるバンクにパーティション分割できます。 つまり、メモリ操作を順を追って完了できます。 メモリ操作が順番に完了するように、ほとんどのプロセッサはメモリ バリア命令を提供します。 完全メモリ バリア により、メモリ バリア命令の前に表示されるメモリの読み取りと書き込み操作は、メモリ バリア命令の後に表示されるメモリの読み取りと書き込み操作の前にメモリにコミットされます。 読み取りメモリ バリア はメモリ読み取り操作のみを並べ替え、書き込みメモリ バリア メモリ書き込み操作のみを並べ替えます。 これらの命令により、バリア全体でメモリ操作を並べ替えることができる最適化がコンパイラによって確実に無効になります。
プロセッサは、取得、解放、フェンスセマンティクスを使用してメモリ バリアの命令をサポートできます。 これらのセマンティクスは、操作の結果が使用可能になる順序を表します。 取得セマンティクスでは、操作の結果は、コード内の操作の後に表示される操作の結果の前に使用できます。 リリース セマンティクスでは、操作の結果は、コード内の前に表示される操作の結果の後で使用できます。 フェンス セマンティクスは、取得セマンティクスとリリース セマンティクスを組み合わせたものです。 フェンス セマンティクスを使用した操作の結果は、コード内の操作の後に表示される操作の結果の前と、その前に表示される操作の後に表示されます。
SSE2 をサポートする x86 および x64 プロセッサでは、命令は mfence (メモリ フェンス)、lfence (ロード フェンス)、および sfence (ストア フェンス) を します。 ARM プロセッサでは、侵入は dmb され、dsb されます。 詳細については、プロセッサのドキュメントを参照してください。
次の同期関数では、適切なバリアを使用してメモリの順序を確認します。
- 重要なセクションを入力または終了する関数
- SRW ロックを取得または解放する関数
- 1 回限りの初期化の開始と完了
- EnterSynchronizationBarrier 関数の
- 同期オブジェクトを通知する関数
- 待機関数
- インターロックされた関数 (NoFence サフィックスを持つ関数、または _nf サフィックスを持つ組み込み関数を除く)
競合状態の修正
次のコードは、最初に実行されるプロセッサがメイン メモリに fValueHasBeenComputed
を書き込む前にメイン メモリ CacheComputedValue
書き込む可能性があるため、マルチプロセッサ システムで競合状態 iValue
。 したがって、FetchComputedValue
を同時に実行する 2 番目のプロセッサは、TRUEとして fValueHasBeenComputed
読み取りますが、iValue
の新しい値は、まだ最初のプロセッサのキャッシュにあり、メモリに書き込まれていません。
int iValue;
BOOL fValueHasBeenComputed = FALSE;
extern int ComputeValue();
void CacheComputedValue()
{
if (!fValueHasBeenComputed)
{
iValue = ComputeValue();
fValueHasBeenComputed = TRUE;
}
}
BOOL FetchComputedValue(int *piResult)
{
if (fValueHasBeenComputed)
{
*piResult = iValue;
return TRUE;
}
else return FALSE;
}
上記の競合状態は、volatile キーワードまたは InterlockedExchange 関数を使用して、fValueHasBeenComputed
の値が TRUE に設定される前にすべてのプロセッサに対して iValue
の値更新されるようにすることで修復できます。
Visual Studio 2005 以降では、/volatile:ms モードでコンパイルされた場合、コンパイラは揮発性 変数 読み取り操作の取得セマンティクスと、揮発性 変数に対する書き込み操作の解放セマンティクスを使用します (CPU でサポートされている場合)。 そのため、次のように例を修正できます。
volatile int iValue;
volatile BOOL fValueHasBeenComputed = FALSE;
extern int ComputeValue();
void CacheComputedValue()
{
if (!fValueHasBeenComputed)
{
iValue = ComputeValue();
fValueHasBeenComputed = TRUE;
}
}
BOOL FetchComputedValue(int *piResult)
{
if (fValueHasBeenComputed)
{
*piResult = iValue;
return TRUE;
}
else return FALSE;
}
Visual Studio 2003 では、揮発性 参照を する揮発性 を します。コンパイラは、揮発性 変数アクセス 順序を変更しません。 ただし、これらの操作はプロセッサによって並べ替えられる可能性があります。 そのため、次のように例を修正できます。
int iValue;
BOOL fValueHasBeenComputed = FALSE;
extern int ComputeValue();
void CacheComputedValue()
{
if (InterlockedCompareExchange((LONG*)&fValueHasBeenComputed,
FALSE, FALSE)==FALSE)
{
InterlockedExchange ((LONG*)&iValue, (LONG)ComputeValue());
InterlockedExchange ((LONG*)&fValueHasBeenComputed, TRUE);
}
}
BOOL FetchComputedValue(int *piResult)
{
if (InterlockedCompareExchange((LONG*)&fValueHasBeenComputed,
TRUE, TRUE)==TRUE)
{
InterlockedExchange((LONG*)piResult, (LONG)iValue);
return TRUE;
}
else return FALSE;
}
関連トピック