同期とマルチプロセッサに関する問題
アプリケーションは、単一プロセッサ システムでのみ有効であるという前提に基づいているため、マルチプロセッサ システムで実行すると問題が発生する可能性があります。
スレッドの優先度
2 つのスレッドがあり、一方のスレッドが他方よりも高い優先順位を持つプログラムを考えてみましょう。 単一プロセッサ システムでは、スケジューラによって優先順位の高いスレッドが優先されるため、優先順位の高いスレッドが優先順位の低いスレッドに制御を譲ることはありません。 マルチプロセッサ システムでは、両方のスレッドをそれぞれ独自のプロセッサで同時に実行できます。
競合状態を避けるために、アプリケーションはデータ構造へのアクセスを同期する必要があります。 優先順位の高いスレッドが、優先順位の低いスレッドからの干渉なしに実行されることを前提とするコードは、マルチプロセッサ システムでは失敗します。
メモリ オーダリング
プロセッサがメモリの場所に書き込むと、パフォーマンスを向上させるためにその値がキャッシュされます。 同様に、プロセッサは、パフォーマンスを向上させるためにキャッシュからの読み取り要求を満たそうとします。 さらに、プロセッサは、アプリケーションによって要求される前に、メモリから値をフェッチし始めます。 これは、投機的実行の一環として、またはキャッシュ ラインの問題が原因で発生する可能性があります。
CPU キャッシュは、並列でアクセスできるバンクにパーティション分割できます。 これは、メモリ操作が順序どおりに実行されない可能性があることを意味します。 メモリ操作を順序どおりに完了させるために、ほとんどのプロセッサではメモリ バリア命令が提供されています。 "完全なメモリ バリア" により、メモリ バリア命令の前に現れるメモリの読み取りと書き込み操作が、メモリ バリア命令の後に現れるどのメモリの読み取りと書き込み操作よりも前にメモリにコミットされることが保証されます。 "読み取りメモリ バリア" はメモリ読み取り操作のみを順序付け、"書き込みメモリ バリア" はメモリ書き込み操作のみを順序付けします。 これらの命令により、バリアを越えてメモリ操作を並べ替える可能性があるコンパイラの最適化が無効化されることも保証されます。
プロセッサでは、取得、解放、フェンスの各セマンティクスを備えたメモリ バリアの命令がサポートされます。 これらのセマンティクスは、操作の結果が使用可能になる順序を表します。 取得のセマンティクスでは、操作の結果はコード内でそれより後に現れる任意の操作の結果よりも前に使用可能になります。 解放のセマンティクスでは、操作の結果はコード内でそれより前に現れる任意の操作の結果よりも後に使用可能になります。 フェンスのセマンティクスは、取得と解放の両方のセマンティクスを組み合わせたものです。 フェンスのセマンティクスを使用した操作の結果は、コード内でそれより後に現れる任意の操作の前と、それより前に現れる任意の操作の後に使用可能になります。
SSE2 をサポートする x86 および x64 プロセッサでは、命令は mfence (メモリ フェンス)、lfence (ロード フェンス)、sfence (ストア フェンス) です。 ARM プロセッサでは、命令は dmb と dsb です。 詳細については、プロセッサのドキュメントを参照してください。
次の同期関数は、適切なバリアを使用してメモリの順序を確保します。
- クリティカル セクションに出入りする関数
- SRW ロックを取得または解放する関数
- 1 回限りの初期化の開始と完了
- EnterSynchronizationBarrier 関数
- 同期オブジェクトに信号を送る関数
- Wait 関数
- Interlocked 関数 (NoFence サフィックスを持つ関数、または _nf サフィックスを持つ組み込み関数を除く)
競合状態の修正
次のコードは、マルチプロセッサ システムでは競合状態になります。その理由は、最初に CacheComputedValue
を実行するプロセッサが iValue
をメイン メモリに書き込む前に fValueHasBeenComputed
をメイン メモリに書き込む可能性があるからです。 その結果、同時に FetchComputedValue
を実行する 2 番目のプロセッサは fValueHasBeenComputed
を TRUE として読み取りますが、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 モードでコンパイルされた場合、コンパイラでは volatile 変数の読み取り操作に対しては取得のセマンティクスが使用され、volatile 変数の書き込み操作に対しては解放のセマンティクスが使用されます (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 では、volatile から volatile への参照は順序付けられます。コンパイラによって volatile 変数のアクセスが再順序付けされることはありません。 ただし、これらの操作はプロセッサによって再順序付けされる可能性があります。 したがって、この例は次のように修正できます。
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;
}
関連トピック