web-dev-qa-db-ja.com

i ++はスレッドセーフではないと聞きましたが、++ iはスレッドセーフですか?

I ++はスレッドセーフステートメントではないと聞いたことがあります。Assemblyでは、元の値を一時としてどこかに保存し、増分してから置き換え、コンテキストスイッチによって中断される可能性があるためです。

しかし、私は++ iについて疑問に思っています。私が知る限り、これは 'add r1、r1、1'などの単一のAssembly命令に縮小され、1つの命令にすぎないため、コンテキストスイッチによって中断することはできません。

誰でも明確にできますか? x86プラットフォームが使用されていると仮定しています。

89
samoz

あなたは間違って聞いたことがあります。 _"i++"_は特定のコンパイラおよび特定のプロセッサアーキテクチャに対してスレッドセーフである可能性がありますが、標準ではまったく義務付けられていません。実際、マルチスレッドはISO CまたはC++標準の一部ではないため (a)、コンパイルするものに基づいてスレッドセーフであると考えることはできません。

_++i_が次のような任意のシーケンスにコンパイルされる可能性は十分にあります。

_load r0,[i]  ; load memory into reg 0
incr r0      ; increment reg 0
stor [i],r0  ; store reg 0 back to memory
_

これは、メモリ増分命令を持たない(想像上の)CPUではスレッドセーフではありません。または、スマートで、次のようにコンパイルできます。

_lock         ; disable task switching (interrupts)
load r0,[i]  ; load memory into reg 0
incr r0      ; increment reg 0
stor [i],r0  ; store reg 0 back to memory
unlock       ; enable task switching (interrupts)
_

lockは割り込みを無効にし、unlockは割り込みを有効にします。しかし、それでも、これらの複数のCPUがメモリを共有しているアーキテクチャでは、これはスレッドセーフではない場合があります(lockは1つのCPUの割り込みのみを無効にする場合があります)。

言語自体(または言語に組み込まれていない場合はそのライブラリ)はスレッドセーフな構造を提供するため、生成されるマシンコードの理解(または誤解)に依存するのではなく、それらを使用する必要があります。

Java synchronizedおよびpthread_mutex_lock()(一部のオペレーティングシステムでC/C++で利用可能)など)を調べる必要があります。 (a)


(a) この質問は、C11およびC++ 11標準が完成する前に尋ねられました。これらの反復により、アトミックデータ型を含む言語仕様にスレッドサポートが導入されました(ただし、これらのスレッドおよびスレッドは一般にオプション少なくともCでは)。

155
paxdiablo

++ iまたはi ++のいずれかについて包括的なステートメントを作成することはできません。どうして? 32ビットシステムで64ビット整数をインクリメントすることを検討してください。基礎となるマシンにクアッドワードの「ロード、インクリメント、ストア」命令がなければ、その値をインクリメントするには複数の命令が必要になりますが、そのいずれもスレッドコンテキストスイッチによって中断できます。

加えて、 ++iは常に「値に1を加える」とは限りません。 Cのような言語では、ポインターをインクリメントすると、実際に指しているもののサイズが追加されます。つまり、iが32バイト構造体へのポインターである場合、++iは32バイトを追加します。ほとんどすべてのプラットフォームにはアトミックな「メモリアドレスでの増分値」命令がありますが、すべてのアトミックな「メモリアドレスの値に任意の値を追加する」命令があるわけではありません。

42
Jim Mischel

どちらもスレッドに対して安全ではありません。

CPUはメモリで直接計算することはできません。メモリから値をロードし、CPUレジスタで計算を行うことにより、間接的にそれを行います。

i ++

register int a1, a2;

a1 = *(&i) ; // One cpu instruction: LOAD from memory location identified by i;
a2 = a1;
a1 += 1; 
*(&i) = a1; 
return a2; // 4 cpu instructions

++ i

register int a1;

a1 = *(&i) ; 
a1 += 1; 
*(&i) = a1; 
return a1; // 3 cpu instructions

どちらの場合も、予測不能なi値をもたらす競合状態があります。

たとえば、それぞれがレジスタa1、b1を使用する2つの並行++ iスレッドがあると仮定します。また、次のようにコンテキスト切り替えを実行します。

register int a1, b1;

a1 = *(&i);
a1 += 1;
b1 = *(&i);
b1 += 1;
*(&i) = a1;
*(&i) = b1;

その結果、iはi + 2にならず、i + 1になりますが、これは正しくありません。

これを改善するために、最新のCPUは、コンテキスト切り替えが無効になっている間、何らかの種類のLOCK、UNLO​​CK CPU命令を提供します。

Win32では、InterlockedIncrement()を使用してスレッドセーフのためにi ++を実行します。ミューテックスに依存するよりもはるかに高速です。

14
yogman

マルチコア環境のスレッド間でintを共有する場合、適切なメモリバリアが必要です。これは、インターロックされた命令(たとえば、win32のInterlockedIncrementを参照)を使用するか、特定のスレッドセーフ保証を行う言語(またはコンパイラー)を使用することを意味します。 CPUレベルの命令の順序変更とキャッシュおよびその他の問題については、これらの保証がない限り、スレッド間で共有されるものが安全であると想定しないでください。

編集:ほとんどのアーキテクチャで想定できることの1つは、適切に配置された単一の単語を扱っている場合、マッシュされた2つの値の組み合わせを含む単一のWordにはならないということです。 2つの書き込みが互いの上に発生した場合、一方が勝ち、もう一方が破棄されます。注意すれば、これを利用して、シングルライター/マルチリーダーの状況で++ iまたはi ++がスレッドセーフであることを確認できます。

11
Eclipse

C++でアトミックな増分が必要な場合は、C++ 0xライブラリ(std::atomicデータ型)またはTBBなど。

かつてGNUコーディングガイドラインは、1つのWordに適合するデータ型を更新することは「通常は安全」だと言っていましたが、そのアドバイスはSMPマシンにとって間違っています。 一部のアーキテクチャでは間違っている、 最適化コンパイラを使用する場合は間違っています。


「1ワードデータ型の更新」コメントを明確にするには:

SMPマシン上の2つのCPUが同じサイクルで同じメモリ位置に書き込みを行ってから、変更を他のCPUとキャッシュに伝播しようとする可能性があります。 1ワードのデータのみが書き込まれ、書き込みが完了するまでに1サイクルしかかからない場合でも、同時に発生するため、どの書き込みが成功するかを保証できません。部分的に更新されたデータは取得しませんが、このケースを処理する他の方法がないため、1回の書き込みは消えます。

比較とスワップは複数のCPU間で適切に調整されますが、1ワードのデータ型のすべての変数割り当てが比較とスワップを使用すると信じる理由はありません。

また、最適化コンパイラはhowロード/ストアのコンパイルには影響しませんが、変更することができますwhenロード/ストアが発生し、読み取りおよびソースコードに表示されるのと同じ順序で発生するように書き込みます(最も有名なダブルチェックロックはVanilla C++では機能しません)。

[〜#〜] note [〜#〜]私の元の答えは、Intel 64ビットアーキテクチャが64ビットデータの処理で壊れていたとも言っていました。それは真実ではないので、答えを編集しましたが、私の編集ではPowerPCチップが壊れていると主張しました。 即値(つまり、定数)をレジスターに読み込む場合に該当 (リスト2およびリスト4の「ポインターのロード」という2つのセクションを参照)。ただし、1サイクルでメモリからデータをロードする命令(lmw)があるため、答えの一部を削除しました。

8
Max Lybbert

C/C++のx86/Windowsでは、スレッドセーフであると想定しないでください。アトミック操作が必要な場合は、 InterlockedIncrement() および InterlockedDecrement() を使用する必要があります。

4
i_am_jorf

プログラミング言語がスレッドについて何も言っていないが、マルチスレッドプラットフォームで実行されている場合、any言語構成はどのようにスレッドセーフになりますか?

他の人が指摘したように、プラットフォーム固有の呼び出しによって、変数へのマルチスレッドアクセスを保護する必要があります。

プラットフォームの特異性を抽象化するライブラリがあり、今後のC++標準はスレッドに対処するためにメモリモデルを適合させています(したがって、スレッドの安全性を保証できます)。

3
xtofl

メモリ内の値を直接インクリメントする単一のAssembly命令に減らしても、スレッドセーフではありません。

メモリ内の値をインクリメントするとき、ハードウェアは「read-modify-write」操作を実行します。メモリから値を読み取り、インクリメントし、メモリに書き戻します。 x86ハードウェアには、メモリ上で直接インクリメントする方法はありません。 RAM(およびキャッシュ)は値の読み取りと保存のみが可能で、値の変更はできません。

ここで、別々のソケット上にあるか、1つのソケットを共有する(共有キャッシュの有無にかかわらず)2つの別々のコアがあるとします。最初のプロセッサが値を読み取り、更新された値を書き戻す前に、2番目のプロセッサが値を読み取ります。両方のプロセッサが値を書き戻した後、2回ではなく1回だけインクリメントされます。

この問題を回避する方法があります。 x86プロセッサ(およびほとんどのマルチコアプロセッサ)は、この種のハードウェアの競合を検出してシーケンス処理することができるため、読み取り-変更-書き込みシーケンス全体がアトミックに見えます。ただし、これは非常にコストがかかるため、コードによって要求された場合にのみ行われます。x86では通常、LOCKプレフィックスを使用します。他のアーキテクチャでは、これを他の方法で行うことができ、同様の結果が得られます。たとえば、ロードリンク/ストア条件付きおよびアトミックな比較とスワップ(最近のx86プロセッサにもこの最後のものがあります)。

ここでvolatileを使用しても効果がないことに注意してください。変数が外部で変更された可能性があることをコンパイラに伝え、その変数への読み取りをレジスタにキャッシュしたり、最適化してはいけません。コンパイラにアトミックプリミティブを使用させません。

最適な方法は、アトミックプリミティブを使用するか(コンパイラまたはライブラリにそれらがある場合)、またはアセンブリで直接インクリメントを行うことです(正しいアトミック命令を使用して)。

3
CesarB

増分がアトミック操作にコンパイルされると想定しないでください。 InterlockedIncrementまたは同様の機能がターゲットプラットフォームに存在するものを使用します。

編集:この特定の質問を調べたところ、X86での増分はシングルプロセッサシステムではアトミックですが、マルチプロセッサシステムではアトミックではありません。ロックプレフィックスを使用するとアトミックになりますが、InterlockedIncrementを使用するだけではるかに移植性が高くなります。

2
Dan Olson

この アセンブリレッスン x86では、メモリー位置に原子的にレジスタを追加できますコードがアトミックに「++ i」または「i ++」を実行する可能性があります。しかし、別の投稿で述べたように、C ANSIは '++'の操作に原子性を適用しないため、コンパイラが何を生成するのか確信が持てません。

1
Selso Liberado

1998年のC++標準では、スレッドについては何も言うことはありませんが、次の標準(今年または次の予定)にはあります。したがって、実装を参照せずに、操作のスレッドセーフについてインテリジェントなことを言うことはできません。使用されているプロセッサだけでなく、コンパイラ、OS、およびスレッドモデルの組み合わせです。

反対にドキュメントがない場合、特にマルチコアプロセッサ(またはマルチプロセッサシステム)では、アクションがスレッドセーフであるとは思いません。また、スレッド同期の問題は偶然にしか発生しないため、テストを信頼することもありません。

使用している特定のシステム向けのドキュメントがある場合を除き、スレッドセーフはありません。

1
David Thornley

スレッドローカルストレージにiをスローします。アトミックではありませんが、問題ではありません。

0
vmguy

「i ++」という式がステートメント内で唯一の場合、「++ i」と同等であり、コンパイラーは一時的な値を保持しないなど十分にスマートであると思います。 「どちらを使用するかを尋ねる必要はありません)、どちらを使用しても、それらはほとんど同じです(美観を除く)。

とにかく、インクリメント演算子がアトミックであっても、正しいロックを使用しない場合、計算の残りが一貫していることを保証しません。

自分で実験したい場合は、N個のスレッドが共有変数をそれぞれM倍ずつ増分するプログラムを作成します。値がN * Mより小さい場合、一部の増分が上書きされます。プリインクリメントとポストインクリメントの両方で試してみてください;-)

0
fortran

カウンターについては、非ロックおよびスレッドセーフの両方である比較とスワップのイディオムを使用することをお勧めします。

これはJavaにあります:

public class IntCompareAndSwap {
    private int value = 0;

    public synchronized int get(){return value;}

    public synchronized int compareAndSwap(int p_expectedValue, int p_newValue){
        int oldValue = value;

        if (oldValue == p_expectedValue)
            value = p_newValue;

        return oldValue;
    }
}

public class IntCASCounter {

    public IntCASCounter(){
        m_value = new IntCompareAndSwap();
    }

    private IntCompareAndSwap m_value;

    public int getValue(){return m_value.get();}

    public void increment(){
        int temp;
        do {
            temp = m_value.get();
        } while (temp != m_value.compareAndSwap(temp, temp + 1));

    }

    public void decrement(){
        int temp;
        do {
            temp = m_value.get();
        } while (temp > 0 && temp != m_value.compareAndSwap(temp, temp - 1));

    }
}
0
AtariPete