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


90

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

しかし、++ iについて疑問に思っています。私の知る限り、これは 'add r1、r1、1'などの単一のアセンブリ命令に削減されます。これは1つの命令のみであるため、コンテキストスイッチによって中断されません。

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


ただの質問です。2つ(またはそれ以上)のスレッドがそのような変数にアクセスするには、どのようなシナリオが必要ですか?私は率直に言って、批判はしません。たった今この時、頭には何も思いつきません。
OscarRyz 2009年

5
オブジェクト数を維持するC ++クラスのクラス変数?
paxdiablo 2009年

1
別の男が私に言ったので私が今日見たばかりの事柄についての良いビデオ:youtube.com/watch
?v=mrvAqvtWYb4

1
C / C ++として再タグ付け。ここではJavaは考慮されていません。C#も同様ですが、厳密に定義されたメモリセマンティクスが不足しています。
Tim Williscroft 2009年

1
@Oscar Reyes i変数を使用する2つのスレッドがあるとします。スレッド1が特定の時点にある場合にのみスレッドを増加させ、別の時点にある場合にのみスレッドを減少させる場合、スレッドの安全性について心配する必要があります。
samoz 2009年

回答:


157

あなたは間違っていると聞いた。"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)

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

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

(a)を調べる必要があるのは、Java synchronizedpthread_mutex_lock()(一部のオペレーティングシステムではC / C ++で利用可能)などです。


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


8
明確な答えは言うまでもなく、これはプラットフォーム固有の問題ではないことを強調するための+1 ...
RBerteig

2
Cシルバーバッジおめでとう:)
Johannes Schaub-litb 2009

私はあなたが何も現代のOSは、割り込みをオフにするには、ユーザーモードのプログラムを許可しないことを正確にすべき、とpthread_mutex_lockの()Cの一部ではないと思う
バスティアンレオナール

@Bastien、何も現代のOSはメモリのインクリメント命令を持っている:-)しかし、あなたのポイントのはC.について取られていなかったCPU上で実行されていないことになる
paxdiablo

5
@Bastien:ブル。RISCプロセッサには通常、メモリインクリメント命令がありません。load / add / storトリプレットは、PowerPCなどでそれを行う方法です。
derobert

42

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

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


35
もちろん、退屈な32ビット整数に制限しない場合、C ++のような言語では、++ iは実際にはデータベースの値を更新するWebサービスの呼び出しになります。
Eclipseの

16

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

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 ++をスレッドセーフにします。ミューテックスに依存するよりもはるかに高速です。


6
「CPUはメモリを使って直接計算を行うことはできません」-これは正確ではありません。CPUがあり、最初にレジスタにロードする必要なしに、メモリ要素に対して「直接」に数学を実行できます。例えば。MC68000
darklon

1
LOCKおよびUNLOCK CPU命令は、コンテキストスイッチとは関係ありません。キャッシュラインをロックします。
David Schwartz

11

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

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


intアクセス(読み取り/書き込み)がアトミックである環境では、事実上間違っています。このような環境で機能するアルゴリズムはいくつかありますが、メモリバリアがないために、古いデータで作業していることがあります。
MSalters 2009年

2
私はただ、原子性がスレッドの安全性を保証するものではないと言っています。ロックフリーのデータ構造またはアルゴリズムを設計できるほど賢い場合は、先に進んでください。ただし、コンパイラが提供する保証が何であるかを知る必要があります。
Eclipseの

10

C ++でアトミックな増分が必要な場合は、C ++ 0xライブラリ(std::atomicデータ型)またはTBBのようなものを使用できます。

かつてGNUコーディングガイドラインで、1語に収まるデータ型の更新は「通常は安全」であると述べられていましたが、そのアドバイスはSMPマシン、一部のアーキテクチャ、および最適化コンパイラを使用する場合は誤りです。


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

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

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

また、最適化コンパイラーはロード/ストアのコンパイル方法に影響を与えませんが、ロード/ストアが発生すると変更される可能性があるため、読み取りと書き込みがソースコードに表示されるのと同じ順序で発生すると予想される場合、深刻な問題を引き起こします(最も有名なダブルチェックロックは、バニラC ++では機能しません)。

注: 私の最初の答えは、Intel 64ビットアーキテクチャは64ビットデータの処理で壊れているとも述べています。それは真実ではないので、私は答えを編集しましたが、私の編集はPowerPCチップが壊れていると主張しました。 これは、即値(つまり、定数)をレジスターに読み込むときにも当てはまります(リスト2とリスト4の「ポインターの読み込み」という2つのセクションを参照)。しかし、1サイクルでメモリからデータをロードするための指示(lmw)があるため、私の答えのその部分を削除しました。


SMPや最適化コンパイラを使用していても、データが自然にアラインされて正しいサイズであれば、ほとんどの最新のCPUでは読み取りと書き込みはアトミックです。ただし、特に64ビットマシンでは多くの注意事項があるため、データがすべてのマシンの要件を満たしていることを確認するのは面倒な場合があります。
ダン・オルソン

更新していただきありがとうございます。正しく、読み取りと書き込みは不完全に完了できないと述べているのでアトミックですが、あなたのコメントは、実際にこの事実にどのように取り組むかを強調しています。メモリバリアと同じように、それらは操作のアトミックな性質には影響を与えませんが、実際のアプローチには影響しません。
ダン・オルソン


4

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

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

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


4

値がメモリ内で直接インクリメントされる単一のアセンブリ命令に削減されたとしても、スレッドセーフではありません。

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

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

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

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

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


2

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

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


1
InterlockedIncrement()はWindows関数です。私のすべてのLinuxボックスと最新のOS Xマシンはx64ベースであるため、InterlockedIncrement()はx86コードよりも「はるかに移植性が高い」と言っても、見た目が悪くなります。
ピートカーカム

Cはアセンブリよりもはるかに移植性が高いのと同じ意味で、はるかに移植性があります。ここでの目標は、特定のプロセッサ用に生成された特定のアセンブリに依存しないようにすることです。他のオペレーティングシステムが問題である場合、InterlockedIncrementは簡単にラップされます。
Dan Olson

2

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


1

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

特に反対の文書がない場合、特にマルチコアプロセッサ(またはマルチプロセッサシステム)でのアクションはスレッドセーフであるとは思いません。スレッド同期の問題は偶然にしか発生しない可能性が高いため、テストも信用しません。

使用している特定のシステム用であるというドキュメントがない限り、スレッドセーフになるものはありません。


1

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


1

私の知る限り、C ++標準によれば、読み取り/書き込みintはアトミックです。

ただし、これが行うことは、データの競合に関連する未定義の動作を取り除くことです。

ただし、両方のスレッドがインクリメントしようとすると、データの競合が発生しますi

次のシナリオを想像してみてください。

i = 0最初にみましょう:

スレッドAはメモリから値を読み取り、独自のキャッシュに格納します。スレッドAは値を1ずつ増やします。

スレッドBはメモリから値を読み取り、独自のキャッシュに格納します。スレッドBは値を1ずつ増やします。

これがすべて単一のスレッドである場合i = 2、メモリに入るでしょう。

ただし、両方のスレッドでは、各スレッドが変更を書き込むため、スレッドAがi = 1メモリに書き戻し、スレッドBがi = 1メモリに書き込みます。

それは明確に定義されており、オブジェクトの部分的な破壊や構築、またはあらゆる種類の引き裂きはありませんが、それでもデータの競合です。

アトミックにインクリメントiするには、以下を使用できます。

std::atomic<int>::fetch_add(1, std::memory_order_relaxed)

この操作がどこで行われるかは気にしないため、インクリメント操作がアトミックであることだけが緩和された順序付けを使用できます。


0

「それは1つの命令にすぎず、コンテキストの切り替えによって中断されない」と言います。-それはすべて単一のCPUに適していますが、デュアルコアCPUはどうですか?そうすれば、コンテキストを切り替えることなく、2つのスレッドが同時に同じ変数にアクセスできます。

言語を知らなければ、答えはその中身をテストすることです。


4
テストによってスレッドセーフかどうかはわかりません-スレッドの問題は100万回に1回発生する可能性があります。ドキュメントで調べます。ドキュメントでスレッドセーフが保証されていない場合は、保証されません。
Eclipse

2
ここで@Joshに同意します。何かがスレッドセーフであるのは、基礎となるコードの分析を通じて数学的に証明できる場合のみです。これに近づくためのテストの量を増やすことはできません。
Rex M

最後の文までは素晴らしい答えでした。
Rob K

0

「i ++」という式がステートメント内にのみある場合、それは「++ i」と同等であり、コンパイラーは一時的な値を保持しないように十分スマートです。したがって、それらを交換可能に使用できる場合(そうでなければどちらを使用するかを尋ねる必要はありません)、ほとんど同じであるため、どちらを使用しても問題ありません(美学を除く)。

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

自分で実験したい場合は、N個のスレッドが共有変数を同時にM回インクリメントするプログラムを作成します...値がN * M未満の場合、一部のインクリメントが上書きされました。プリインクリメントとポストインクリメントの両方で試して、教えてください;-)


0

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

ここでは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));

    }
}

test_and_set関数に似ているようです。
samoz 2009年

1
あなたは「非ロック」と書いたが、「同期」はロックを意味しないのか?
Corey Trager、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.