6

値が部分的にではなく全体的に読み書きされるようにするために使用されるというのは、原子性についての私の理解です。たとえば、実際には 2 つの 32 ビット DWORD (ここでは x86 を想定) である 64 ビット値は、両方の DWORD が同時に読み取り/書き込みされるように、スレッド間で共有されるときにアトミックである必要があります。そうすれば、1 つのスレッドは、更新されていない半分の変数を読み取ることができません。どのようにして原子性を保証しますか?

さらに、揮発性はスレッドの安全性をまったく保証しないことを理解しています。本当?

単純にアトミック/揮発性であることはスレッドセーフであることを多くの場所で暗示しているのを見てきました。私はそれがどのように見えません。他のスレッドで実際に読み取り/書き込みが保証される前に、アトミックまたはその他の値が読み取り/書き込みされるようにするために、メモリバリアも必要ではないでしょうか?

たとえば、中断されたスレッドを作成し、いくつかの値をスレッドで使用可能な構造体に変更するためにいくつかの計算を行ってから、再開するとします。次に例を示します。

HANDLE hThread = CreateThread(NULL, 0, thread_entry, (void *)&data, CREATE_SUSPENDED, NULL);
data->val64 = SomeCalculation();
ResumeThread(hThread);

これは、ResumeThread のメモリ バリアに依存すると思いますか? val64の連動交換はするべきでしょうか?スレッドが実行されていた場合、それはどのように変化しますか?

私はここで多くのことを尋ねていると確信していますが、基本的に私が理解しようとしているのは、タイトルで尋ねたことです。ありがとう

4

2 に答える 2

6

値が全体的に読み書きされることを確認するために使用されます

それはアトミック性のほんの一部です。その核心は、「割り込み不可」を意味し、プロセッサ上の命令であり、その副作用を別の命令とインターリーブすることはできません。設計上、メモリ更新は、単一のメモリ バス サイクルで実行できる場合、アトミックです。これには、単一のサイクルで更新できるように、メモリ位置のアドレスを揃える必要があります。アラインされていないアクセスでは、バイトの一部が 1 つのサイクルで書き込まれ、一部が別のサイクルで書き込まれる、余分な作業が必要になります。これで、中断できなくなりました。

整列された更新を取得するのは非常に簡単です。これは、コンパイラによって提供される保証です。または、より広義には、コンパイラによって実装されたメモリ モデルによって。これは単に整列されたメモリアドレスを選択するだけで、次の変数を整列させるために意図的に数バイトの未使用のギャップを残すことがあります。プロセッサのネイティブ ワード サイズより大きい変数への更新は、決してアトミックにはなりません。

しかし、はるかに重要なのは、スレッド化を機能させるために必要なプロセッサ命令の種類です。すべてのプロセッサは、CAS 命令の一種であるコンペア アンド スワップを実装しています。これは、同期を実装するために必要なコア アトミック命令です。モニター (別名条件変数)、ミューテックス、シグナル、クリティカル セクション、セマフォなどの高レベルの同期プリミティブはすべて、そのコア命令の上に構築されます。

これが最小です。プロセッサは通常、単純な操作をアトミックにするために追加のものを提供します。変数のインクリメントと同様に、読み取り-変更-書き込み操作が必要なため、その核心は割り込み可能な操作です。アトミックである必要があることは非常に一般的です。ほとんどの C++ プログラムは、たとえば参照カウントを実装するためにアトミックに依存しています。

揮発性はスレッドの安全性をまったく保証しません

そうではありません。これは、マシンがプロセッサ コアを 1 つしか持っていなかった、はるかに楽な時代にさかのぼる属性です。これは、コード生成にのみ影響します。特に、コード オプティマイザがメモリ アクセスを排除し、代わりにプロセッサ レジスタ内の値のコピーを使用しようとする方法に影響します。コードの実行速度に大きな違いをもたらします。レジスタから値を読み取ると、メモリから読み取るよりも簡単に 3 倍速くなります。

volatileを適用すると、コード オプティマイザはレジスタ内の値が正確であるとは見なさず、強制的にメモリを再度読み取るようになります。それ自体では安定しない種類のメモリ値、つまりメモリ マップド I/O を介してレジスタを公開するデバイスでのみ問題になります。Itanium が最もひどい例である、メモリ モデルが弱いプロセッサの上にセマンティクスを置こうとするコアの意味以来、それはひどく悪用されてきました。今日のvolatileで得られるものは、使用する特定のコンパイラとランタイムに大きく依存します。スレッドセーフのために使用しないでください。代わりに常に同期プリミティブを使用してください。

単にアトミック/揮発性であることはスレッドセーフです

それが本当なら、プログラミングはずっと簡単になるでしょう。アトミック操作は非常に単純な操作のみをカバーします。実際のプログラムでは、多くの場合、オブジェクト全体をスレッドセーフに保つ必要があります。すべてのメンバーをアトミックに更新し、部分的に更新されたオブジェクトのビューを決して公開しません。リストの繰り返しのような単純なものは中心的な例です。その要素を見ている間、別のスレッドでリストを変更することはできません。その場合は、安全に処理できるようになるまでコードをブロックできる、より高レベルの同期プリミティブに到達する必要があります。

実際のプログラムは、この同期の必要性に苦しむことが多く、アムダールの法則の動作を示します。つまり、余分なスレッドを追加しても、実際にはプログラムが高速になるわけではありません。時には実際に遅くすることもあります。これより優れたネズミ捕りを見つけた人は誰でもノーベル賞が保証されます。

于 2015-01-31T13:26:48.747 に答える
2

一般に、C および C++ は、マルチスレッド プログラムで「揮発性」オブジェクトの読み取りまたは書き込みがどのように動作するかについて保証しません。(「新しい」C++11 は、標準の一部としてスレッドを含むようになったため、おそらくそうしますが、伝統的にスレッドは標準の C または C++ の一部ではありませんでした。)ポータブルであることは問題です。特定のコンパイラとプラットフォームが「揮発性」オブジェクトへのアクセスをスレッドセーフな方法で処理するかどうかについては、がらくたです。

一般的な規則は次のとおりです。「揮発性」は、スレッド セーフなアクセスを保証するには不十分です。スレッド共有の値に安全にアクセスするには、プラットフォームが提供するメカニズム (通常は関数または同期オブジェクト) を使用する必要があります。

現在、具体的には Windows、具体的には VC++ 2005+ コンパイラ、具体的には x86 および x64 システムでは、プリミティブ オブジェクト (int など) へのアクセスは、次の場合にスレッド セーフにすることができます。

  1. 64 ビットおよび 32 ビットの Windows では、オブジェクトは 32 ビット型である必要があり、32 ビットでアラインされている必要があります。
  2. 64 ビット Windows では、オブジェクトも 64 ビット型である可能性があり、64 ビットでアラインされている必要があります。
  3. volatile と宣言する必要があります。

これらが当てはまる場合、オブジェクトへのアクセスは揮発性でアトミックになり、キャッシュの一貫性を保証する命令に囲まれます。オブジェクトにアクセスするときにアトミック操作を実行するコードをコンパイラが作成できるように、サイズとアラインメントの条件が満たされている必要があります。オブジェクトを volatile と宣言することで、コンパイラがレジスタに読み取った可能性のある以前の値のキャッシュに関連するコードの最適化を行わず、生成されたコードにアクセス時に適切なメモリ バリア命令が含まれるようにします。

それでも、小さなものにアクセスするには Interlocked* 関数のようなものを使用し、より大きなオブジェクトやデータ構造には Mutexes や CriticalSections などの標準的な同期オブジェクトを使用したほうがよいでしょう。理想的には、ライブラリを取得して、適切なロックが既に含まれているデータ構造を使用します。ライブラリと OS に可能な限りハードワークを任せてください。

あなたの例では、スレッドがまだ開始されているかどうかに関係なく、スレッドセーフアクセスを使用して val64 を更新する必要があると思います。

スレッドが既に実行されている場合は、InterchangeExchange64 などを使用するか、適切なメモリ バリア命令を実行する何らかの同期オブジェクトを取得して解放することにより、何らかのスレッドセーフな val64 への書き込みが確実に必要になります。同様に、スレッドはそれを読み取るためにスレッドセーフなアクセサーを使用する必要があります。

スレッドがまだ再開されていない場合は、少しわかりにくくなります。ResumeThread が同期関数を使用またはそのように動作し、メモリ バリア操作を実行する可能性がありますが、ドキュメントではそのように指定されていないため、そうではないと想定することをお勧めします。

参考文献:

32 ビットおよび 64 ビット整列型の原子性について... https://msdn.microsoft.com/en-us/library/windows/desktop/ms684122%28v=vs.85%29.aspx

メモリフェンスを含む「揮発性」について... https://msdn.microsoft.com/en-us/library/windows/desktop/ms686355%28v=vs.85%29.aspx

于 2015-01-31T11:36:01.057 に答える