int
、double
などのC/C++基本型、アトミック、スレッドセーフ?
データの競合はありませんか。つまり、あるスレッドがそのようなタイプのオブジェクトに書き込み、別のスレッドがそのオブジェクトから読み取る場合、動作は明確に定義されていますか?
そうでない場合は、コンパイラまたは他の何かに依存していますか?
いいえ、基本的なデータ型(例:int
、double
)はアトミックではありません。 std::atomic
を参照してください。
代わりに、std::atomic<int>
またはstd::atomic<double>
を使用できます。
注:std::atomic
はC++ 11で導入されましたが、C++ 11より前は、C++標準はマルチスレッドの存在をまったく認識していませんでした。
@Joshが指摘したように、 std::atomic_flag
はアトミックなブール型です。 std::atomic
スペシャライゼーションとは異なり、ロックフリーであることを保証です。
引用されたドキュメントは、 http://open-std.org/JTC1/SC22/WG21/docs/papers/2015/n4567.pdf からのものです。標準は無料ではないので、これは最終/公式バージョンではありません。
- 2つの式の評価の一方がメモリロケーション(1.7)を変更し、もう一方が同じメモリロケーションを読み取りまたは変更した場合、式の評価は競合します。
- このライブラリは、同期操作として特別に識別される多数のアトミック操作(29節)およびmutexの操作(30節)を定義します。これらの操作は、あるスレッド内の割り当てを別のスレッドから見えるようにする特別な役割を果たします。 1つ以上のメモリ位置での同期操作は、消費操作、取得操作、解放操作、または取得操作と解放操作の両方です。関連付けられたメモリ位置のない同期操作はフェンスであり、獲得フェンス、解放フェンス、または獲得フェンスと解放フェンスの両方にすることができます。さらに、同期操作ではない緩和されたアトミック操作と、特別な特性を持つアトミックな読み取り-変更-書き込み操作があります。
- 次の場合、2つのアクションが同時に発生する可能性があります
(23.1)—異なるスレッドによって実行される、または
(23.2)—これらはシーケンスされておらず、少なくとも1つはシグナルハンドラーによって実行されます。
プログラムの実行には、少なくとも1つがアトミックではなく、以下で説明するシグナルハンドラの特別な場合を除いて、どちらもアトミックではない2つの潜在的な同時競合アクションが含まれる場合、データの競合が含まれます。そのようなデータの競合は、未定義の動作をもたらします。
- 整数型 `` char、
signed char
、unsigned char
、short
、unsigned short
、int
、unsigned int
、long
、unsigned long
、long long
、unsigned long long
、char16_
t、char32_t
、wchar_t
、およびその他の必要なタイプヘッダー<cstdint>
のtypedefによって。整数型の積分ごとに、特殊化atomic<integral>
は、整数型に適した追加のアトミック操作を提供します。 29.6.1で指定されている一般的なアトミック操作を提供する特殊化atomic<bool>
が必要です。
- アトミッククラステンプレートには、ポインターの部分的な特殊化が必要です。これらの特殊化には、標準レイアウト、単純なデフォルトコンストラクタ、および単純なデストラクタが必要です。それらはそれぞれ集約初期化構文をサポートしなければならない。
- Atomic_flag型のオブジェクトに対する操作はロックフリーでなければなりません。 [注:したがって、操作にもアドレスを使用しないでください。ロックフリー操作を必要とする型は他にないため、atomic_flag型は、この国際標準に準拠するために必要な最小限のハードウェア実装型です。残りの型はatomic_flagでエミュレートできますが、理想的なプロパティではありません。 —終了ノート]
Cは(現在)タグに含まれていなくても質問で言及されているため、 C Standard は次のように述べています。
5.1.2.3プログラムの実行
...
シグナルの受信によって抽象マシンの処理が中断されると、ロックフリーのアトミックオブジェクトでもタイプ
volatile sig_atomic_t
でもないオブジェクトの値は、浮動小数点環境の状態と同様に指定されません。ロックフリーのアトミックオブジェクトでも、_volatile sig_atomic_t
型でもない、ハンドラーによって変更されたオブジェクトの値は、ハンドラーが変更された場合の浮動小数点環境の状態と同様に、ハンドラーの終了時に不定になります元の状態に復元されません。
そして
5.1.2.4マルチスレッド実行とデータ競合
...
2つの式の評価conflictのいずれかがメモリ位置を変更し、もう一方が同じメモリ位置を読み取りまたは変更した場合。
[標準のいくつかのページ-アトミック型を明示的に扱ういくつかの段落]
プログラムの実行には、異なるスレッドで2つの競合するアクションが含まれる場合、data raceが含まれます。これらのアクションの少なくとも1つはアトミックではなく、どちらも前に発生しません。 そのようなデータ競合は未定義の動作をもたらします。
シグナルが処理を中断する場合、値は「不定」であり、明示的にアトミックではない型への同時アクセスは未定義の動作であることに注意してください。
アトミック、アトムのプロパティで何かを記述するもの。単語atomはラテン語に由来します atomus は「分割されていない」という意味です。
通常、アトミック操作(言語に関係なく)には次の2つの性質があると思います。
つまりそれは分割不可能な方法で実行され、私はこれがOPが「スレッドセーフ」と呼ぶものだと信じています。ある意味では、操作は別のスレッドから見たときに瞬時に発生します。
たとえば、次の操作はおそらく分割されています(コンパイラ/ハードウェアに依存):
i += 1;
(仮想的なハードウェアとコンパイラ上の)別のスレッドが次のように観測できるためです。
load r1, i;
addi r1, #1;
store i, r1;
適切な同期なしで上記の操作i += 1
を実行する2つのスレッドは、間違った結果を生成する場合があります。最初にi=0
と言い、スレッドT1
がT1.r1 = 0
をロードし、スレッドT2
がt2.r1 = 0
をロードします。両方のスレッドはそれぞれのr1
sを1増やし、結果をi
に保存します。 2つのインクリメントが実行されましたが、インクリメント操作が割り切れたため、i
の値は1のままです。 i+=1
の前後に同期があった場合、他のスレッドは操作が完了するまで待機していたため、分割されていない操作が観察されることに注意してください。
単純な書き込みであっても、分割されていない場合とそうでない場合があることに注意してください。
i = 3;
store i, #3;
コンパイラとハードウェアに依存します。たとえば、i
のアドレスが適切にアライメントされていない場合、アライメントされていないロード/ストアを使用する必要があり、CPUによっていくつかの小さなロード/ストアとして実行されます。
非アトミック操作は並べ替えられる場合があり、プログラムのソースコードに記述された順序で必ずしも発生するわけではありません。
たとえば、 "as-if"ルール の下では、揮発性メモリへのすべてのアクセスがプログラムで指定された順序で発生する限り、コンパイラは適切と思われるストアとロードを並べ替えることができます。 「あたかも」のように、プログラムは標準の文言に従って評価されました。そのため、非スレッド操作は、マルチスレッドプログラムでの実行順序に関する仮定を破って再配置される場合があります。これが、マルチスレッドプログラミングでシグナル変数として生のint
を一見無害に使用できない理由です。たとえ書き込みと読み取りが分割できない場合でも、コンパイラによっては順序によってプログラムが壊れる場合があります。アトミック操作は、指定されたメモリセマンティクスに応じて、その周りの操作の順序を強制します。 std::memory_order
を参照してください。
CPUは、そのCPUのメモリ順序の制約の下で、メモリアクセスを並べ替えることもあります。 x86アーキテクチャのメモリ順序の制約は、 Intel 64およびIA32アーキテクチャソフトウェア開発者マニュアル 22ページから始まるセクション8.2で見つけることができます。
int
、char
など)はアトミックではありません特定の条件下で、分割不可能なストアおよびロード命令、または場合によっては算術命令を使用する場合でも、ストアおよびロードの順序を保証しないためです。そのため、適切な同期なしでマルチスレッドコンテキストで使用することは安全ではなく、他のスレッドによって観察されたメモリ状態がその時点で想定しているものであることを保証します。
これが説明することを願っていますなぜプリミティブ型はアトミックではありません。
私が見たことのない追加情報は、これまでの他の回答で言及されていました:
たとえば、std::atomic<bool>
を使用し、bool
が実際にターゲットアーキテクチャ上でアトミックである場合、コンパイラは冗長なフェンスまたはロックを生成しません。プレーンbool
の場合と同じコードが生成されます。
言い換えると、std::atomic
を使用しても、プラットフォームで正確性を確保するために実際に必要な場合にのみコードの効率が低下します。したがって、それを避ける理由はありません。