これらのスレッドはシングルコアCPUで実行されると想定しています。CPUは1サイクルで1つの命令のみを実行します。つまり、CPUリソースを共有しているとさえ考えられます。しかし、コンピューターは1回限りの指示を保証します。マルチスレッドにロックは不要ですか?
これらのスレッドはシングルコアCPUで実行されると想定しています。CPUは1サイクルで1つの命令のみを実行します。つまり、CPUリソースを共有しているとさえ考えられます。しかし、コンピューターは1回限りの指示を保証します。マルチスレッドにロックは不要ですか?
回答:
これは例を挙げて説明するのが最適です。
並列に複数回実行する単純なタスクがあり、たとえばWebページのヒットをカウントするなど、タスクが実行された回数をグローバルに追跡したいとします。
各スレッドがカウントをインクリメントするポイントに到達すると、その実行は次のようになります。
すべてのスレッドは、このプロセスのどの時点でも中断できることに注意してください。したがって、スレッドAがステップ1を実行し、その後、スレッドBが3つすべてのステップを実行した後に中断すると、スレッドAが再開すると、レジスタのヒット数が間違ってしまいます。レジスタが復元され、古い番号が喜んでインクリメントされますヒットの、およびその増加した数を格納します。
さらに、スレッドAが中断されている間に他のスレッドがいくつでも実行される可能性があるため、スレッドAが最後に書き込むカウントは、正しいカウントを大きく下回る可能性があります。
そのため、スレッドがステップ1を実行する場合、他のスレッドがステップ1を実行する前にステップ3を実行する必要があることを確認する必要があります。 、プロセスの完了後にのみロックを解放します。これにより、コードのこの「クリティカルセクション」を誤ってインターリーブすることができず、誤ったカウントが発生します。
しかし、操作がアトミックな場合はどうでしょうか?
はい、魔法のユニコーンと虹の土地では、インクリメント操作はアトミックであるため、上記の例ではロックは必要ありません。
ただし、魔法のユニコーンと虹の世界ではほとんど時間を費やさないことを理解することが重要です。ほとんどすべてのプログラミング言語では、インクリメント操作は上記の3つのステップに分けられます。プロセッサがアトミックインクリメント操作をサポートしている場合でも、その操作は非常に高価です。メモリから読み取り、数値を変更し、メモリに書き戻す必要があります...通常、アトミックインクリメント操作は、失敗する可能性があります。つまり、上記の単純なシーケンスをループに置き換える必要があります(以下で説明します)。
マルチスレッドコードでも、多くの変数は単一のスレッドに対してローカルに保持されるため、各変数が単一のスレッドに対してローカルであると想定すると、プログラムははるかに効率的であり、プログラマーがスレッド間の共有状態を保護できるようになります。特に、後で説明するように、アトミック操作は通常、スレッドの問題を解決するのに十分ではないことを考えると。
揮発性変数
この特定の問題に対するロックを回避したい場合、最初の例で示されている手順は、実際に最新のコンパイルされたコードで起こることではないことを最初に認識しなければなりません。コンパイラは変数を変更しているのは1つのスレッドだけであると想定しているため、各スレッドは、プロセッサレジスタが他の何かに必要になるまで、変数のキャッシュコピーを保持します。キャッシュされたコピーがある限り、メモリに戻って再度読み取る必要はないと想定します(高価になります)。また、変数がレジスタに保持されている限り、変数をメモリに書き戻しません。
変数をvolatileとしてマークすると、最初の例で示した状況(上記で特定したすべてのスレッドの問題)に戻ることができます。これは、この変数が他のユーザーによって変更されているため、または、アクセスまたは変更されるたびにメモリに書き込まれます。
したがって、volatileとしてマークされた変数は、アトミックインクリメント操作の土地に連れて行くことはありません。
増分をアトミックにする
volatile変数を使用したら、最新のCPUがサポートする低レベルの条件付きセット操作(多くの場合、compare and setまたはcompare and swapと呼ばれます)を使用して、インクリメント操作をアトミックにします。このアプローチは、たとえばJavaのAtomicIntegerクラスで採用されています。
197 /**
198 * Atomically increments by one the current value.
199 *
200 * @return the updated value
201 */
202 public final int incrementAndGet() {
203 for (;;) {
204 int current = get();
205 int next = current + 1;
206 if (compareAndSet(current, next))
207 return next;
208 }
209 }
上記のループは、手順3が成功するまで、次の手順を繰り返し実行します。
ステップ3が失敗した場合(ステップ1の後で別のスレッドによって値が変更されたため)、再びメインメモリから直接変数を読み取り、再試行します。
比較およびスワップ操作は高価ですが、この場合はロックを使用するよりもわずかに優れています。スレッドがステップ1の後に中断される場合、ステップ1に到達する他のスレッドはブロックして最初のスレッドを待つ必要がないためですコストのかかるコンテキストスイッチングを防ぐことができます。最初のスレッドが再開すると、変数の最初の書き込み試行で失敗しますが、変数を再読み取りすることで続行できます。これは、ロックに必要なコンテキストスイッチよりもコストが低い可能性があります。
そのため、実際のロックを使用せずに、比較とスワップを介して、アトミックインクリメント(または単一の変数に対する他の操作)の領域に到達できます。
では、ロックが厳密に必要なのはいつですか?
アトミック操作で複数の変数を変更する必要がある場合、ロックが必要になります。そのための特別なプロセッサ命令は見つかりません。
単一の変数で作業しており、失敗して変数を読み取って最初からやり直さなければならない作業に備えている限り、compare-and-swapで十分です。
各スレッドが最初に変数Xに2を追加し、次にXに2を掛ける例を考えてみましょう。
Xが最初に1で、2つのスレッドが実行される場合、結果は(((1 + 2)* 2)+ 2)* 2 = 16になると予想されます。
ただし、スレッドがインターリーブする場合、すべての操作がアトミックであっても、代わりに両方の加算が最初に発生し、乗算が後になって、(1 + 2 + 2)* 2 * 2 = 20になります。
これは、乗算と加算が可換演算ではないために発生します。
そのため、操作自体がアトミックであるだけでは不十分です。操作の組み合わせをアトミックにする必要があります。
ロックを使用してプロセスをシリアル化するか、計算を開始したときに1つのローカル変数を使用してXの値を保存し、2番目のローカル変数を中間ステップとして使用してから、compare-and-swapを使用してそれを行うことができますXの現在の値がXの元の値と同じ場合にのみ新しい値を設定します。失敗した場合は、Xを読み取り、計算を再度実行することからやり直す必要があります。
いくつかのトレードオフがあります:計算が長くなると、実行中のスレッドが中断される可能性が高くなり、再開する前に別のスレッドによって値が変更されるため、障害が発生しやすくなり、無駄になりますプロセッサー時間。非常に長時間の計算を伴う多数のスレッドの極端な場合、100個のスレッドが変数を読み取り、計算に従事する場合があります。その場合、最初の完了のみが新しい値の書き込みに成功し、他の99個のスレッドは引き続き計算を完了しますが、完了時に値を更新できないことを発見します。その時点で、それぞれが値を読み取り、計算をやり直します。残りの99個のスレッドで同じ問題が繰り返され、膨大な量のプロセッサー時間が浪費される可能性があります。
そのような状況では、ロックを介したクリティカルセクションの完全なシリアル化がはるかに優れています。ロックを取得できなかった場合、99個のスレッドが中断し、ロックポイントに到着する順に各スレッドを実行します。
シリアル化が重要ではない場合(増分の場合のように)、数値の更新に失敗した場合に失われる計算が最小限である場合、compare-and-swap操作を使用することで大きな利点が得られる可能性があります。ロックよりも安価です。
この引用を考慮してください:
一部の人々は、問題に直面したとき、「私は知っている、スレッドを使用する」と考え、その後、彼らは2つの問題を抱えています
ご存知のように、CPUで1つの命令が実行される場合でも、コンピュータープログラムは、アトミックアセンブリー命令よりもはるかに多く構成されます。したがって、たとえば、コンソール(またはファイル)に書き込むと、意図したとおりに動作するようにロックする必要があります。
多くの答えがロックを説明しようとしたようですが、OPが必要とするのはマルチタスクが実際に何であるかの説明だと思います。
CPUが1つでもシステム上で複数のスレッドを実行している場合、これらのスレッドのスケジュール方法を指示する2つの主な方法論があります(つまり、シングルコアCPUで実行するように配置)。
問題は個々の操作にあるのではなく、操作が実行するより大きなタスクにあります。
多くのアルゴリズムは、動作する状態を完全に制御できるという前提で記述されています。説明したようなインターリーブされた順序付き実行モデルでは、操作は互いに任意にインターリーブされる場合があり、それらが状態を共有する場合、状態が一貫性のない形になるリスクがあります。
不変式を一時的に中断する可能性のある関数と比較して、それらの機能を実行できます。中間状態が外部から観察可能でない限り、タスクを達成するために必要なことは何でもできます。
並行コードを作成する場合、競合状態に排他的にアクセスしない限り、競合状態が安全でないと見なされるようにする必要があります。排他的アクセスを実現する一般的な方法は、ロックを保持するなど、同期プリミティブで同期することです。
一部のプラットフォームで同期プリミティブが発生する傾向があるもう1つのことは、メモリバリアを発行することです。これにより、メモリ間のCPU間の一貫性が保証されます。
'bool'を設定することを除いて、変数の読み取りまたは書き込みが1つの命令だけを取るという保証はありません(少なくともcでは)。または、読み取り/書き込みの途中で中断することはできません。
bool
このプロパティのみを持つことができますか?また、メモリからの読み込み、変更、メモリへのプッシュバックについて話しているのですか、それともレジスタレベルで話しているのですか?レジスタへの読み取り/書き込みはすべて中断されませんが、mem load、mem storeは中断されません(それだけで2命令であり、少なくとも1つは値を変更するためです)。
the standard says that only 'bool' needs to be safe against a context switch in the middle of a read/write of a single variable
は本当に答えに追加されるべきです。
CPUは一度に1つの命令を実行しますが、2つ以上のCPUがある場合はどうなりますか?
アトミック命令を利用するようにプログラムを書くことができれば、ロックは必要ないという点であなたは正しいです:実行が与えられたプロセッサ上で中断できず、他のプロセッサによる干渉を受けない命令。
複数の命令を干渉から保護する必要があり、同等のアトミック命令がない場合は、ロックが必要です。
たとえば、ノードを二重リンクリストに挿入するには、いくつかのメモリロケーションを更新する必要があります。挿入前および挿入後、特定の不変式はリストの構造について保持します。ただし、挿入中、これらの不変式は一時的に壊れます。リストは「作成中」の状態です。
別のスレッドが不変式の間にリストを行進するか、またはそのような状態にあるときにリストを変更しようとすると、データ構造が破損し、動作が予測不能になります:ソフトウェアがクラッシュするか、誤った結果が続く可能性があります。そのため、リストが更新されているときに、スレッドが何らかの方法で互いの邪魔にならないことに同意する必要があります。
適切に設計されたリストは、アトミックな命令で操作できるため、ロックは不要です。このアルゴリズムは「ロックフリー」と呼ばれます。ただし、アトミック命令は実際にはロックの形式であることに注意してください。これらはハードウェアに特別に実装され、プロセッサ間の通信を介して機能します。アトミックではない同様の命令よりも高価です。
アトミック命令の贅沢さに欠けるマルチプロセッサでは、単純なメモリアクセスとポーリングループで相互排除のプリミティブを構築する必要があります。このような問題は、Edsger DijkstraやLeslie Lamportなどが取り組んできました。