複数のスレッドがシングルコアCPUでロックを必要とする理由を説明できますか?


18

これらのスレッドはシングルコアCPUで実行されると想定しています。CPUは1サイクルで1つの命令のみを実行します。つまり、CPUリソースを共有しているとさえ考えられます。しかし、コンピューターは1回限りの指示を保証します。マルチスレッドにロックは不要ですか?


ソフトウェアトランザクションメモリはまだ主流ではないからです。
dan_waterworth

@dan_waterworthソフトウェアのトランザクショナルメモリは、重要な複雑性レベルではひどく失敗するので、そうですか?;)
メイソンウィーラー

リッチ・ヒッキーはそれに反対するに違いない。
ロバートハーヴェイ

@MasonWheeler、非自明なロックは驚くほどうまく機能し、追跡するのが難しい微妙なバグの原因になったことはありませんか?STMは、自明でない複雑さのレベルでうまく機能しますが、競合がある場合は問題があります。このような場合、STMのより制限的な形式であるこのようなものの方が優れています。ところで、タイトルが変更されたので、私がコメントした理由を解決するのに時間がかかりました。
dan_waterworth

回答:


32

これは例を挙げて説明するのが最適です。

並列に複数回実行する単純なタスクがあり、たとえばWebページのヒットをカウントするなど、タスクが実行された回数をグローバルに追跡したいとします。

各スレッドがカウントをインクリメントするポイントに到達すると、その実行は次のようになります。

  1. ヒット数をメモリからプロセッサレジスタに読み込む
  2. その数を増やします。
  3. その番号をメモリに書き戻す

すべてのスレッドは、このプロセスのどの時点でも中断できることに注意してください。したがって、スレッド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が成功するまで、次の手順を繰り返し実行します。

  1. 揮発性変数の値をメモリから直接読み取ります。
  2. その値を増やします。
  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操作を使用することで大きな利点が得られる可能性があります。ロックよりも安価です。


しかし、カウンターincreamentがアトミックである場合、ロックは必要でしたか?
pythonee

@pythonee:カウンターの増分がアトミックである場合、可能性はありません。しかし、合理的なサイズのマルチスレッドプログラムでは、共有リソースで非アトミックタスクを実行する必要があります。
ドックブラウン

1
コンパイラー組み込み関数を使用して増分をアトミックにしない限り、おそらくそうではありません。
マイクラーセン

はい、read / modify(increment)/ writeがアトミックである場合、その操作にはロックは不要です。DEC-10 AOSE(1を追加し、結果== 0の場合はスキップ)命令は、テストおよび設定セマフォとして使用できるように、特にアトミックに作成されました。マニュアルでは、36ビットのレジスタを最後までロールするのに数日間連続してカウントするマシンを使用するので十分であると述べています。しかし、今、あなたがするすべてが「メモリに追加」されるわけではありません。
ジョンR.ストローム

これらの懸念のいくつかに対処するために回答を更新しました:はい、操作をアトミックにすることができます十分かつ完全なシリアル化が必要です。ロックは、完全なシリアル化を実現するために知っている唯一のメカニズムです。
セオドアマードック

4

この引用を考慮してください:

一部の人々は、問題に直面したとき、「私は知っている、スレッドを使用する」と考え、その後、彼らは2つの問題を抱えています

ご存知のように、CPUで1つの命令が実行される場合でも、コンピュータープログラムは、アトミックアセンブリー命令よりもはるかに多く構成されます。したがって、たとえば、コンソール(またはファイル)に書き込むと、意図したとおりに動作するようにロックする必要があります。


引用はスレッドではなく正規表現だと思いましたか?
user16764

3
私にとっては、引用はスレッドにはるかに当てはまるようです(スレッドの問題のために単語/文字が順不同で印刷されています)。しかし、現在、出力に余分な「s」があります。これは、コードに3つの問題があることを示しています。
セオドアマードック

1
その副作用。非常にまれに1プラス1を追加して4294967295を取得できます:)
gbjbaanb

3

多くの答えがロックを説明しようとしたようですが、OPが必要とするのはマルチタスクが実際に何であるかの説明だと思います。

CPUが1つでもシステム上で複数のスレッドを実行している場合、これらのスレッドのスケジュール方法を指示する2つの主な方法論があります(つまり、シングルコアCPUで実行するように配置)。

  • 協調マルチタスク-Win9xで使用するには、各アプリケーションが明示的に制御を放棄する必要がありました。この場合、スレッドAが何らかのアルゴリズムを実行している限り、決して中断されないことが保証されるため、ロックについて心配する必要はありません。
  • プリエンプティブマルチタスク -ほとんどの最新のOS(Win2k以降)で使用されます。これはタイムスライスを使用し、スレッドがまだ作業を行っている場合でもスレッドを中断します。これは、単一のスレッドがマシン全体をハングさせることはないため、はるかに堅牢です。これは、協調的なマルチタスク処理の本当の可能性でした。一方、ロックについて心配する必要があるのは、いつでもスレッドの1つが中断(プリエンプト)され、OSが別のスレッドの実行をスケジュールする可能性があるためです。この動作でマルチスレッドアプリケーションをコーディングする場合、コードのすべての行(またはすべての命令)の間で異なるスレッドが実行される可能性があることを考慮する必要があります。現在、単一のコアであっても、データの一貫した状態を確保するためにロックが非常に重要になります。

0

問題は個々の操作にあるのではなく、操作が実行するより大きなタスクにあります。

多くのアルゴリズムは、動作する状態を完全に制御できるという前提で記述されています。説明したようなインターリーブされた順序付き実行モデルでは、操作は互いに任意にインターリーブされる場合があり、それらが状態を共有する場合、状態が一貫性のない形になるリスクがあります。

不変式を一時的に中断する可能性のある関数と比較して、それらの機能を実行できます。中間状態が外部から観察可能でない限り、タスクを達成するために必要なことは何でもできます。

並行コードを作成する場合、競合状態に排他的にアクセスしない限り、競合状態が安全でないと見なされるようにする必要があります。排他的アクセスを実現する一般的な方法は、ロックを保持するなど、同期プリミティブで同期することです。

一部のプラットフォームで同期プリミティブが発生する傾向があるもう1つのことは、メモリバリアを発行することです。これにより、メモリ間のCPU間の一貫性が保証されます。


0

'bool'を設定することを除いて、変数の読み取りまたは書き込みが1つの命令だけを取るという保証はありません(少なくともcでは)。または、読み取り/書き込みの途中で中断することはできません。


32ビット整数を設定するのにどれくらいの命令がかかりますか?
DXM

1
最初の声明について少し話していただけますか。boolだけがアトミックに読み書きできることを意味しますが、それは意味がありません。「bool」は実際にはハードウェアには存在しません。通常、バイトまたはワードのいずれかとして実装されます。したがって、どのようにboolこのプロパティのみを持つことができますか?また、メモリからの読み込み、変更、メモリへのプッシュバックについて話しているのですか、それともレジスタレベルで話しているのですか?レジスタへの読み取り/書き込みはすべて中断されませんが、mem load、mem storeは中断されません(それだけで2命令であり、少なくとも1つは値を変更するためです)。
コービン

1
hyperhreaded / multicore / branch-predicted / multi-cached CPUの単一命令の概念は少し注意が必要です-しかし、標準では、読み取り/書き込みの途中でコンテキストスイッチに対して安全である必要があるのは「bool」のみであると述べています単一の変数の。ブースト::アトミック他の種類の周りにミューテックスをラップし、私はC ++ 11にはいくつかのより多くのスレッドguarranteesを追加すると思いますがあります
マーティンベケット

説明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は本当に答えに追加されるべきです。
ウルフ

0

共有メモリ。

定義は... スレッド:共有メモリを備えた多数の並行プロセスです。

共有メモリがない場合、それらは通常old-school-UNIXプロセスと呼ばれます。
ただし、共有ファイルにアクセスするときには、ロックが必要になる場合があります。

(UNIXのようなカーネルの共有メモリは、実際には通常、共有メモリアドレスを表す偽のファイル記述子を使用して実装されていました)


0

CPUは一度に1つの命令を実行しますが、2つ以上のCPUがある場合はどうなりますか?

アトミック命令を利用するようにプログラムを書くことができれば、ロックは必要ないという点であなたは正しいです:実行が与えられたプロセッサ上で中断できず、他のプロセッサによる干渉を受けない命令。

複数の命令を干渉から保護する必要があり、同等のアトミック命令がない場合は、ロックが必要です。

たとえば、ノードを二重リンクリストに挿入するには、いくつかのメモリロケーションを更新する必要があります。挿入前および挿入後、特定の不変式はリストの構造について保持します。ただし、挿入中、これらの不変式は一時的に壊れます。リストは「作成中」の状態です。

別のスレッドが不変式の間にリストを行進するか、またはそのような状態にあるときにリストを変更しようとすると、データ構造が破損し、動作が予測不能になります:ソフトウェアがクラッシュするか、誤った結果が続く可能性があります。そのため、リストが更新されているときに、スレッドが何らかの方法で互いの邪魔にならないことに同意する必要があります。

適切に設計されたリストは、アトミックな命令で操作できるため、ロックは不要です。このアルゴリズムは「ロックフリー」と呼ばれます。ただし、アトミック命令は実際にはロックの形式であることに注意してください。これらはハードウェアに特別に実装され、プロセッサ間の通信を介して機能します。アトミックではない同様の命令よりも高価です。

アトミック命令の贅沢さに欠けるマルチプロセッサでは、単純なメモリアクセスとポーリングループで相互排除のプリミティブを構築する必要があります。このような問題は、Edsger DijkstraやLeslie Lamportなどが取り組んできました。


参考までに、単一の比較と交換のみを使用して二重リンクリストの更新を処理するロックフリーアルゴリズムを読みました。また、ハードウェアで二重比較とスワップ(68040で実装されたが、他の68xxxプロセッサでは実行されなかった)よりもはるかに安価であると思われる施設に関するホワイトペーパーを読みました。 -linked /store-conditional。2つのリンクされたロードと条件付きストアを許可しますが、2つのストア間で発生するアクセスは最初のロールバックをロールバックしません。それは、二重比較と保存よりも実装がはるかに簡単です
...-supercat

...ただし、二重リンクリストの更新を管理しようとすると、同様の利点があります。私の知る限り、二重リンクロードは受け入れられていませんが、需要があればハードウェアコストはかなり安く見えるでしょう。
-supercat
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.