値が全体的に読み書きされることを確認するために使用されます
それはアトミック性のほんの一部です。その核心は、「割り込み不可」を意味し、プロセッサ上の命令であり、その副作用を別の命令とインターリーブすることはできません。設計上、メモリ更新は、単一のメモリ バス サイクルで実行できる場合、アトミックです。これには、単一のサイクルで更新できるように、メモリ位置のアドレスを揃える必要があります。アラインされていないアクセスでは、バイトの一部が 1 つのサイクルで書き込まれ、一部が別のサイクルで書き込まれる、余分な作業が必要になります。これで、中断できなくなりました。
整列された更新を取得するのは非常に簡単です。これは、コンパイラによって提供される保証です。または、より広義には、コンパイラによって実装されたメモリ モデルによって。これは単に整列されたメモリアドレスを選択するだけで、次の変数を整列させるために意図的に数バイトの未使用のギャップを残すことがあります。プロセッサのネイティブ ワード サイズより大きい変数への更新は、決してアトミックにはなりません。
しかし、はるかに重要なのは、スレッド化を機能させるために必要なプロセッサ命令の種類です。すべてのプロセッサは、CAS 命令の一種であるコンペア アンド スワップを実装しています。これは、同期を実装するために必要なコア アトミック命令です。モニター (別名条件変数)、ミューテックス、シグナル、クリティカル セクション、セマフォなどの高レベルの同期プリミティブはすべて、そのコア命令の上に構築されます。
これが最小です。プロセッサは通常、単純な操作をアトミックにするために追加のものを提供します。変数のインクリメントと同様に、読み取り-変更-書き込み操作が必要なため、その核心は割り込み可能な操作です。アトミックである必要があることは非常に一般的です。ほとんどの C++ プログラムは、たとえば参照カウントを実装するためにアトミックに依存しています。
揮発性はスレッドの安全性をまったく保証しません
そうではありません。これは、マシンがプロセッサ コアを 1 つしか持っていなかった、はるかに楽な時代にさかのぼる属性です。これは、コード生成にのみ影響します。特に、コード オプティマイザがメモリ アクセスを排除し、代わりにプロセッサ レジスタ内の値のコピーを使用しようとする方法に影響します。コードの実行速度に大きな違いをもたらします。レジスタから値を読み取ると、メモリから読み取るよりも簡単に 3 倍速くなります。
volatileを適用すると、コード オプティマイザはレジスタ内の値が正確であるとは見なさず、強制的にメモリを再度読み取るようになります。それ自体では安定しない種類のメモリ値、つまりメモリ マップド I/O を介してレジスタを公開するデバイスでのみ問題になります。Itanium が最もひどい例である、メモリ モデルが弱いプロセッサの上にセマンティクスを置こうとするコアの意味以来、それはひどく悪用されてきました。今日のvolatileで得られるものは、使用する特定のコンパイラとランタイムに大きく依存します。スレッドセーフのために使用しないでください。代わりに常に同期プリミティブを使用してください。
単にアトミック/揮発性であることはスレッドセーフです
それが本当なら、プログラミングはずっと簡単になるでしょう。アトミック操作は非常に単純な操作のみをカバーします。実際のプログラムでは、多くの場合、オブジェクト全体をスレッドセーフに保つ必要があります。すべてのメンバーをアトミックに更新し、部分的に更新されたオブジェクトのビューを決して公開しません。リストの繰り返しのような単純なものは中心的な例です。その要素を見ている間、別のスレッドでリストを変更することはできません。その場合は、安全に処理できるようになるまでコードをブロックできる、より高レベルの同期プリミティブに到達する必要があります。
実際のプログラムは、この同期の必要性に苦しむことが多く、アムダールの法則の動作を示します。つまり、余分なスレッドを追加しても、実際にはプログラムが高速になるわけではありません。時には実際に遅くすることもあります。これより優れたネズミ捕りを見つけた人は誰でもノーベル賞が保証されます。