web-dev-qa-db-ja.com

インターロックされた変数の読み取り

仮定:

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);
_
25
MattJ

これは、「目標は単に_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(少なくとも私にとっては現時点ではダウンしている)の ハーブサッターの効果的な同時実行に関する記事 を読んでください。

13
Michael Burr

あなたのやり方は良いです:

LONG Cur = InterlockedCompareExchange(&_ServerState, 0, 0);

私は同様のソリューションを使用しています:

LONG Cur = InterlockedExchangeAdd(&_ServerState, 0);
7
Sergey

インターロックされた命令は原子性を提供しますおよびプロセッサ間同期。書き込みと読み取りの両方を同期する必要があるため、はい、スレッド間で共有され、ロックで保護されていない値を読み取るには、インターロックされた命令を使用する必要があります。ロックフリープログラミング(そしてそれがあなたがしていることです)は非常にトリッキーな領域なので、代わりにロックを使用することを検討するかもしれません。これが本当にプログラムのボトルネックの1つである場合を除いて、最適化する必要がありますか?

5

このスレッドを再訪する必要がある人には、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
1
Sergey D

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);
0
Neeraj Singh

読み取りは問題ありません。 32ビット値は、キャッシュラインで分割されない限り、常に全体として読み取られます。あなたのalign8はそれが常にキャッシュライン内にあることを保証するのであなたは大丈夫でしょう。

指示の並べ替えやそのすべてのナンセンスを忘れてください。結果は常に順番に削除されます。そうでなければ、プロセッサのリコールになります!!!

デュアルCPUマシン(つまり、最も遅いFSBを介して共有される)の場合でも、CPUはMESIプロトコルを介してキャッシュの一貫性を保証するため、問題はありません。あなたが保証されていない唯一のことはあなたが読んだ値が絶対的に最新ではないかもしれないということです。しかし、とにかく最新のものは何ですか?これは、その読み取り値に基づいてその場所に書き戻さない場合、ほとんどの状況で知る必要がない可能性が高いことです。それ以外の場合は、最初にインターロックされたopsを使用して処理していました。

要するに、読み取りでインターロックされた操作を使用しても何も得られません(コードを保守している次の人に注意深く踏むように思い出させることを除いて、その人は最初からコードを保守する資格がない可能性があります)。

編集:エイドリアンマッカーシーによって残されたコメントに応えて。

コンパイラの最適化の効果を見落としています。コンパイラがすでにレジスタに値があると判断した場合、メモリから再読み取りするのではなく、その値を再利用します。また、コンパイラーは、観察可能な副作用がないと判断した場合、最適化のために命令の並べ替えを行う場合があります。

不揮発性変数からの読み取りが問題ないとは言いませんでした。すべての質問は、インターロックが必要かどうかということでした。実際、問題の変数はvolatileで明確に宣言されています。または、youはキーワードvolatileの効果を見落としていましたか?

0
Zach Saw

あなたすべき大丈夫です。揮発性であるため、オプティマイザーがあなたを野蛮にすることはありません。また、32ビット値であるため、少なくともほぼアトミックである必要があります。考えられる驚きの1つは、命令パイプラインがそれを回避できるかどうかです。

一方、保護されたルーチンを使用するための追加コストはいくらですか?

0
Charlie Martin

現在の値の読み取りには、ロックは必要ありません。

0
Alphaneo

Interlocked *関数は、2つの異なるプロセッサが同じメモリにアクセスするのを防ぎます。シングルプロセッサシステムでは、問題はありません。異なるコアにスレッドがあり、両方がこの値にアクセスしているデュアルコアシステムの場合、インターロック*なしでアトミックと思われることを実行する際に問題が発生する可能性があります。

0
Doug T.