仮定:
A.WIN32でのC++。
B. InterlockedIncrement()
およびInterlockedDecrement()
を使用してインクリメントおよびデクリメントされる適切に整列された揮発性整数。
___declspec (align(8)) volatile LONG _ServerState = 0;
_
単に_ServerStateを読み取りたい場合、InterlockedXXX
関数を介して変数を読み取る必要がありますか?
たとえば、次のようなコードを見たことがあります。
_LONG x = InterlockedExchange(&_ServerState, _ServerState);
_
そして
_LONG x = InterlockedCompareExchange(&_ServerState, _ServerState, _ServerState);
_
目標は、__ServerState
_の現在の値を単純に読み取ることです。
簡単に言うことはできません:
_if (_ServerState == some value)
{
// blah blah blah
}
_
このテーマについては、WRTで混乱が生じているようです。 Windowsではレジスタサイズの読み取りがアトミックであることを理解しているので、InterlockedXXX
関数は不要だと思います。
マットJ。
さて、回答ありがとうございます。ところで、これはVisual C++ 2005および2008です。
それが本当なら、InterlockedXXX
関数を使用して__ServerState
_の値を読み取る必要があります。明確にするために、それを実行するための最良の方法は何ですか?
_LONG x = InterlockedExchange(&_ServerState, _ServerState);
_
これには、値を変更するという副作用があります。私が本当にやりたいのは、値を読み取ることだけです。それだけでなく、InterlockedExchange()
の呼び出しの準備として__ServerState
_の値がスタックにプッシュされるため、コンテキストスイッチがある場合、フラグを間違った値にリセットする可能性があります。 。
_LONG x = InterlockedCompareExchange(&_ServerState, _ServerState, _ServerState);
_
これは、MSDNで見た例から取ったものです。
参照 http://msdn.Microsoft.com/en-us/library/ms686355(VS.85).aspx
私が必要とするのは、線に沿った何かです:
_lock mov eax, [_ServerState]
_
いずれにせよ、私が明確だと思ったポイントは、クリティカルセクションのオーバーヘッドを発生させることなく、フラグへのスレッドセーフなアクセスを提供することです。 LONGがInterlockedXXX()
ファミリーの関数を介してこのように使用されているのを見たことがあるので、私の質問です。
さて、現在の値を読み取るというこの問題の良い解決策は次のとおりです。
_LONG Cur = InterlockedCompareExchange(&_ServerState, 0, 0);
_
これは、「目標は単に_ServerStateの現在の値を読み取ること」の意味と、使用するツールのセットとプラットフォームによって異なります(Win32とC++を指定しますが、どのC++コンパイラは指定しないかによって異なります)。 。
値が破損しないように値を読み取りたいだけの場合(つまり、他のプロセッサが値を0x12345678から0x87654321に変更している場合、読み取りは0x12344321ではなくこれら2つの値のいずれかを取得します)、単に読み取りはOKです。変数が:である限り
volatile
、これはC/C++標準では約束されていませんが、WindowsとMSVCはこれらの保証を行っており、Win32を対象とするほとんどのコンパイラも同様に保証していると思います。
ただし、読み取りを他のスレッドの動作と同期させたい場合は、さらに複雑になります。単純な「メールボックス」プロトコルがあるとします。
struct mailbox_struct {
uint32_t flag;
uint32_t data;
};
typedef struct mailbox_struct volatile mailbox;
// the global - initialized before wither thread starts
mailbox mbox = { 0, 0 };
//***************************
// Thread A
while (mbox.flag == 0) {
/* spin... */
}
uint32_t data = mbox.data;
//***************************
//***************************
// Thread B
mbox.data = some_very_important_value;
mbox.flag = 1;
//***************************
スレッドAは、mbox.dataに有効な情報があることを示すmbox.flagを待機してスピンします。スレッドBは、いくつかのデータをmailbox.dataに書き込み、mbox.dataが有効であることを示すシグナルとしてmbox.flagを1に設定します。
この場合、スレッドAでのmbox.dataの後続の読み取りでスレッドBによって書き込まれた値が取得されなくても、mbox.flagのスレッドAでの単純な読み取りで値1が取得される可能性があります。
これは、コンパイラがスレッドBのmbox.dataおよびmbox.flagへの書き込みを並べ替えない場合でも、プロセッサやキャッシュが並べ替える可能性があるためです。 C/C++は、スレッドBがmbox.flagに書き込む前にmbox.dataに書き込むようにコンパイラーがコードを生成することを保証しますが、プロセッサーとキャッシュは異なる考えを持っている可能性があります-「メモリバリア」または「取得」と呼ばれる特別な処理リリースセマンティクスは、スレッドの命令ストリームのレベルより下の順序を確実にするために使用する必要があります。
MSVC以外のコンパイラが、命令レベルより下の順序について主張しているかどうかはわかりません。ただし、MSは、MSVCの場合はvolatileで十分であることを保証します-MSは、volatile書き込みにはリリースセマンティクスがあり、volatile読み取りには取得セマンティクスがあることを指定しています-これがどのバージョンのMSVCに適用されるかはわかりませんが、 http:// msdn .Microsoft.com/en-us/library/12a04hfd.aspx?ppud = 4 。
また、Interlocked APIを使用して、共有の場所への単純な読み取りと書き込みを実行する、あなたが説明するようなコードを見てきました。私の考えは、InterlockedAPIを使用することです。ロックフリーのスレッド間通信は、理解するのが非常に難しく、微妙な落とし穴に満ちています。バグの診断が非常に困難になる可能性のある重要なコードのショートカットを取得しようとするのは、私には良い考えではないようです。 。また、Interlocked APIを使用すると、コードを管理している人に「これは、他の何かと共有または同期する必要があるデータアクセスです-慎重に踏み込んでください! "。
また、Interlocked APIを使用する場合、ハードウェアとコンパイラの詳細を全体像から外します-プラットフォームは、それらすべてが適切に処理されることを確認します-もう不思議ではありません...
このトピックに関する適切な情報については、DDJ(少なくとも私にとっては現時点ではダウンしている)の ハーブサッターの効果的な同時実行に関する記事 を読んでください。
あなたのやり方は良いです:
LONG Cur = InterlockedCompareExchange(&_ServerState, 0, 0);
私は同様のソリューションを使用しています:
LONG Cur = InterlockedExchangeAdd(&_ServerState, 0);
インターロックされた命令は原子性を提供しますおよびプロセッサ間同期。書き込みと読み取りの両方を同期する必要があるため、はい、スレッド間で共有され、ロックで保護されていない値を読み取るには、インターロックされた命令を使用する必要があります。ロックフリープログラミング(そしてそれがあなたがしていることです)は非常にトリッキーな領域なので、代わりにロックを使用することを検討するかもしれません。これが本当にプログラムのボトルネックの1つである場合を除いて、最適化する必要がありますか?
このスレッドを再訪する必要がある人には、Bartoszがよく説明したことに、標準のアトミックが利用できない場合は_InterlockedCompareExchange()
が標準のatomic_load()
の良い代替手段であると付け加えたいと思います。これは、i86Win64のCでmy_uint32_t_varをアトミックに読み取るためのコードです。 atomic_load()
はベンチマークとして含まれています:
long debug_x64_i = std::atomic_load((const std::_Atomic_long *)&my_uint32_t_var);
00000001401A6955 mov eax,dword ptr [rbp+30h]
00000001401A6958 xor edi,edi
00000001401A695A mov dword ptr [rbp-0Ch],eax
debug_x64_i = _InterlockedCompareExchange((long*)&my_uint32_t_var, 0, 0);
00000001401A695D xor eax,eax
00000001401A695F lock cmpxchg dword ptr [rbp+30h],edi
00000001401A6964 mov dword ptr [rbp-0Ch],eax
debug_x64_i = _InterlockedOr((long*)&my_uint32_t_var, 0);
00000001401A6967 prefetchw [rbp+30h]
00000001401A696B mov eax,dword ptr [rbp+30h]
00000001401A696E xchg ax,ax
00000001401A6970 mov ecx,eax
00000001401A6972 lock cmpxchg dword ptr [rbp+30h],ecx
00000001401A6977 jne foo+30h (01401A6970h)
00000001401A6979 mov dword ptr [rbp-0Ch],eax
long release_x64_i = std::atomic_load((const std::_Atomic_long *)&my_uint32_t_var);
00000001401A6955 mov eax,dword ptr [rbp+30h]
release_x64_i = _InterlockedCompareExchange((long*)&my_uint32_t_var, 0, 0);
00000001401A6958 mov dword ptr [rbp-0Ch],eax
00000001401A695B xor edi,edi
00000001401A695D mov eax,dword ptr [rbp-0Ch]
00000001401A6960 xor eax,eax
00000001401A6962 lock cmpxchg dword ptr [rbp+30h],edi
00000001401A6967 mov dword ptr [rbp-0Ch],eax
release_x64_i = _InterlockedOr((long*)&my_uint32_t_var, 0);
00000001401A696A prefetchw [rbp+30h]
00000001401A696E mov eax,dword ptr [rbp+30h]
00000001401A6971 mov ecx,eax
00000001401A6973 lock cmpxchg dword ptr [rbp+30h],ecx
00000001401A6978 jne foo+31h (01401A6971h)
00000001401A697A mov dword ptr [rbp-0Ch],eax
32ビット読み取り操作は一部の32ビットシステムではすでにアトミックです(Intelの仕様によると、これらの操作はアトミックですが、これが当てはまる保証はありません他のx86互換プラットフォーム)。したがって、これをスレッドの同期に使用しないでください。
何らかのフラグが必要な場合は、その目的で Event
オブジェクトと WaitForSingleObject
関数を使用することを検討する必要があります。
あなたの最初の理解は基本的に正しいです。 Windowsがサポートする(またはサポートする予定の)すべてのMPプラットフォーム)で必要なメモリモデルによると、volatileとマークされた自然整列変数からの読み取りは、サイズよりも小さい限りアトミックですマシンワードの。書き込みと同じ。「ロック」プレフィックスは必要ありません。
インターロックを使用せずに読み取りを行うと、プロセッサの順序が変更される可能性があります。これは、限られた状況でx86でも発生する可能性があります。変数からの読み取りは、別の変数の書き込みの上に移動される場合があります。 Windowsがサポートするほとんどすべての非x86アーキテクチャでは、明示的なインターロックを使用しないと、さらに複雑な並べ替えが必要になります。
比較交換ループを使用している場合は、交換する変数を揮発性としてマークする必要があるという要件もあります。理由を示すコード例を次に示します。
long g_var = 0; // not marked 'volatile' -- this is an error
bool foo () {
long oldValue;
long newValue;
long retValue;
// (1) Capture the original global value
oldValue = g_var;
// (2) Compute a new value based on the old value
newValue = SomeTransformation(oldValue);
// (3) Store the new value if the global value is equal to old?
retValue = InterlockedCompareExchange(&g_var,
newValue,
oldValue);
if (retValue == oldValue) {
return true;
}
return false;
}
うまくいかない可能性があるのは、コンパイラが揮発性でない場合はいつでもg_varからoldValueを再フェッチする権限の範囲内にあることです。この「再実体化」最適化は、レジスタ圧力が高いときにレジスタがスタックにこぼれるのを防ぐことができるため、多くの場合に優れています。
したがって、関数のステップ(3)は次のようになります。
// (3) Incorrectly store new value regardless of whether the global
// is equal to old.
retValue = InterlockedCompareExchange(&g_var,
newValue,
g_var);
読み取りは問題ありません。 32ビット値は、キャッシュラインで分割されない限り、常に全体として読み取られます。あなたのalign8はそれが常にキャッシュライン内にあることを保証するのであなたは大丈夫でしょう。
指示の並べ替えやそのすべてのナンセンスを忘れてください。結果は常に順番に削除されます。そうでなければ、プロセッサのリコールになります!!!
デュアルCPUマシン(つまり、最も遅いFSBを介して共有される)の場合でも、CPUはMESIプロトコルを介してキャッシュの一貫性を保証するため、問題はありません。あなたが保証されていない唯一のことはあなたが読んだ値が絶対的に最新ではないかもしれないということです。しかし、とにかく最新のものは何ですか?これは、その読み取り値に基づいてその場所に書き戻さない場合、ほとんどの状況で知る必要がない可能性が高いことです。それ以外の場合は、最初にインターロックされたopsを使用して処理していました。
要するに、読み取りでインターロックされた操作を使用しても何も得られません(コードを保守している次の人に注意深く踏むように思い出させることを除いて、その人は最初からコードを保守する資格がない可能性があります)。
編集:エイドリアンマッカーシーによって残されたコメントに応えて。
コンパイラの最適化の効果を見落としています。コンパイラがすでにレジスタに値があると判断した場合、メモリから再読み取りするのではなく、その値を再利用します。また、コンパイラーは、観察可能な副作用がないと判断した場合、最適化のために命令の並べ替えを行う場合があります。
不揮発性変数からの読み取りが問題ないとは言いませんでした。すべての質問は、インターロックが必要かどうかということでした。実際、問題の変数はvolatile
で明確に宣言されています。または、youはキーワードvolatile
の効果を見落としていましたか?
あなたすべき大丈夫です。揮発性であるため、オプティマイザーがあなたを野蛮にすることはありません。また、32ビット値であるため、少なくともほぼアトミックである必要があります。考えられる驚きの1つは、命令パイプラインがそれを回避できるかどうかです。
一方、保護されたルーチンを使用するための追加コストはいくらですか?
現在の値の読み取りには、ロックは必要ありません。
Interlocked *関数は、2つの異なるプロセッサが同じメモリにアクセスするのを防ぎます。シングルプロセッサシステムでは、問題はありません。異なるコアにスレッドがあり、両方がこの値にアクセスしているデュアルコアシステムの場合、インターロック*なしでアトミックと思われることを実行する際に問題が発生する可能性があります。