ロックフリーマルチスレッディングは、実際のスレッディングの専門家向けです


86

私はジョン・スキートが質問に与えた答えを読んでいて、その中で彼はこれに言及しました:

私に関する限り、ロックフリーマルチスレッドは実際のスレッドの専門家向けであり、私はその専門家ではありません。

これを聞いたのは初めてではありませんが、ロックフリーのマルチスレッドコードの記述方法を学ぶことに興味がある場合、実際にどのように行うかについて話している人はほとんどいません。

だから私の質問は、スレッド化などについてあなたができるすべてを学ぶことに加えて、ロックフリーのマルチスレッドコードを具体的に書くことをどこから学び始めますか、そしていくつかの良いリソースは何ですか?

乾杯


私はgcc、linux、およびX86 / X68プラットフォームを使用しています。ロックフリーは、それらがすべてそれを鳴らすほど難しくはありません!gccアトミックビルトインにはIntelにメモリバリアがありますが、それは実際には問題ではありません。重要なのは、メモリがアトミックに変更されることです。「ロックフリー」のデータ構造を設計すると、別のスレッドが変更を認識しても問題にならないようになります。単一のリンクリスト、スキップリスト、ハッシュテーブル、フリーリストなどはすべて、ロックフリーで簡単に実行できます。ロックフリーはすべてのためではありません。これは、特定の状況に適したもう1つのツールです。
johnnycrash 2012年


リソースの推奨事項として終了するか、何を求めているのか明確にしないように投票します。
CiroSantilli郝海东冠事病六四事件法轮功2015年

回答:


100

現在の「ロックフリー」実装は、ほとんどの場合同じパターンに従います。

  • *いくつかの状態を読み、そのコピーを作成します**
  • *コピーを変更**
  • 連動操作を行う
  • 失敗した場合は再試行してください

(*オプション:データ構造/アルゴリズムによって異なります)

最後のビットは不気味にスピンロックに似ています。実際、それは基本的なスピンロックです。:)
これについては@nobugzに同意します。ロックフリーマルチスレッドで使用されるインターロック操作のコストは、実行する必要のあるキャッシュタスクとメモリコヒーレンシータスクによって左右さます。

ただし、「ロックフリー」のデータ構造で得られるのは、「ロック」が非常にきめ細かいことです。これにより、2つの同時スレッドが同じ「ロック」(メモリ位置)にアクセスする可能性が低くなります。

ほとんどの場合の秘訣は、専用のロックがないことです。代わりに、たとえば、配列内のすべての要素またはリンクリスト内のすべてのノードを「スピンロック」として扱います。前回の読み取り以降に更新がなかった場合は、読み取り、変更を行い、更新を試みます。あった場合は、再試行します。
これにより、追加のメモリやリソースの要件を導入することなく、「ロック」(ああ、申し訳ありませんが、非ロック:)が非常に細かくなります。
きめ細かくすることで、待機の可能性が低くなります。追加のリソース要件を導入せずに、可能な限りきめ細かくすることは素晴らしいことだと思いませんか?

ただし、楽しみのほとんどは、正しいロード/ストアの順序を確認することから得られます。
直感に反して、CPUはメモリの読み取り/書き込みを自由に並べ替えることができます。ちなみに、CPUは非常にスマートです。単一のスレッドからこれを観察するのは困難です。ただし、複数のコアでマルチスレッドを実行し始めると、問題が発生します。あなたの直感は崩壊します:命令があなたのコードの前にあるからといって、それが実際にもっと早く起こるという意味ではありません。CPUは命令を順不同で処理できます。特に、メモリアクセスのある命令に対してこれを実行して、メインメモリのレイテンシを隠し、キャッシュをより有効に活用することを好みます。

さて、直感に反して、コードのシーケンスが「トップダウン」で流れるのではなく、シーケンスがまったくないかのように実行され、「悪魔の遊び場」と呼ばれることは間違いありません。どのようなロード/ストアの再注文が行われるかについて正確な答えを出すことは不可能だと思います。その代わり、1は常にの面で話すメイズmightsと最悪の事態に準備します。「ああ、CPUこの読み取りをその書き込みの前に来るように並べ替える可能性があるので、この場所にメモリバリアを配置するのが最善です。」

事項であってもこれらの事実によって複雑にされメイズmightsは、 CPUアーキテクチャ間で異なる場合があります。それは可能性がある、例えば、その何か場合も起こらないことが保証1つのアーキテクチャで 起こるかもしれない他の上。


「ロックフリー」のマルチスレッドを正しく行うには、メモリモデルを理解する必要があります。
ただし、このストーリーでMFENCE示されているように、メモリモデルと保証を正しく取得することは簡単ではありません。これにより、IntelとAMDは、JVM開発者の間で混乱引き起こすというドキュメントにいくつかの修正を加えました。結局のところ、開発者が最初から信頼していたドキュメントは、そもそもそれほど正確ではありませんでした。

.NETのロックは暗黙のメモリバリアをもたらすため、それらを安全に使用できます(ほとんどの場合、つまり...たとえば、このJoe Duffy-Brad Abrams-Vance Morrisonの怠惰な初期化、ロック、揮発性物質、およびメモリの素晴らしさを参照してください)障壁。:)(必ずそのページのリンクをたどってください。)

追加のボーナスとして、サイドクエストで.NETメモリモデルを紹介します。:)

VanceMorrisonの「oldiebutgoldie」もあります:すべての開発者がマルチスレッドアプリについて知っておくべきこと

...そしてもちろん、@Ericが述べたように、ジョーダフィーは、件名に決定的な読み取りです。

優れたSTMは、きめ細かいロックにできるだけ近づけることができ、おそらく手作りの実装に近いか同等のパフォーマンスを提供します。そのうちの一つがあるSTM.NETからDevLabsプロジェクトMSの。

あなたが.NETだけの熱狂者でないなら、ダグ・リーはJSR-166でいくつかの素晴らしい仕事をしました
Cliff Clickは、Javaと.NETの同時ハッシュテーブルのように、ロックストライピングに依存しないハッシュテーブルについて興味深い見解を持っており、750CPUまで十分に拡張できるようです。

Linuxの領域に足を踏み入れることを恐れない場合は、次の記事で、現在のメモリアーキテクチャの内部と、キャッシュラインの共有によってパフォーマンスが低下する可能性について詳しく説明しますすべてのプログラマがメモリについて知っておくべきこと

@BenはMPIについて多くのコメントをしました:私はMPIがいくつかの分野で輝くかもしれないことに心から同意します。MPIベースのソリューションは、賢くしようとする中途半端なロックの実装よりも、推論が簡単で、実装が簡単で、エラーが発生しにくい可能性があります。(ただし、主観的には、STMベースのソリューションにも当てはまります。)多くの成功例が示唆しているように、Erlangなどで適切な分散アプリケーションを正しく作成する方が光年簡単であることも間違いありません。

ただし、MPIは、単一のマルチコアシステムで実行されている場合、独自のコストと独自の問題があります。たとえばErlangでは、プロセスのスケジューリングとメッセージキューの同期に関して解決すべき問題があります
また、MPIシステムは通常、そのコアで、「軽量プロセス」のための一種の協調N:Mスケジューリングを実装します。これは、たとえば、軽量プロセス間に避けられないコンテキストスイッチがあることを意味します。これは「古典的なコンテキストスイッチ」ではなく、ほとんどの場合ユーザースペース操作であり、高速化できることは事実です。ただし、インターロック操作にかかる20〜200サイクル未満にできるかどうかは疑わしいです。ユーザーモードのコンテキスト切り替えは確かに遅いIntelMcRTライブラリでも。軽量プロセスによるN:Mスケジューリングは新しいものではありません。LWPはSolarisに長い間存在していました。彼らは放棄されました。NTには繊維がありました。それらは現在ほとんどが遺物です。NetBSDには「アクティベーション」がありました。彼らは放棄されました。Linuxは、N:Mスレッド化に関して独自の見解を持っていました。今ではやや死んでいるようです。
時折、 新しい候補があります。たとえば、IntelのMcRT、または最近ではMicrosoftのConCRTと一緒のユーザーモードスケジューリングです。 最も低いレベルでは、N:MMPIスケジューラーが実行することを実行します。Erlang(または任意のMPIシステム)は、新しいUMSを活用することで、SMPシステムに大きなメリットをもたらす可能性があります。

OPの質問は、ソリューションのメリットと主観的な議論に関するものではないと思いますが、それに答える必要がある場合は、タスクに依存すると思います。低レベルで高性能の基本データ構造を構築するために、単一のシステム多くのコアがローロック/「ロックフリー」技術またはSTMのいずれかで、パフォーマンスの点で最良の結果をもたらし、上記のしわが解消されたとしても、パフォーマンスの面でいつでもMPIソリューションを打ち負かすでしょう。例:Erlang。 単一のシステムで実行される適度に複雑なものを構築する場合は、古典的な粗粒度のロックを選択するか、パフォーマンスが非常に重要な場合はSTMを選択します。 分散システムを構築するために、MPIシステムはおそらく自然な選択をするでしょう。(彼らはアクティブではないように見えるが)。


.NET用のMPI実装 もあることに注意してください


1
この回答には多くの優れた情報がありますが、ロックフリーアルゴリズムとデータ構造は本質的に非常に細かいスピンロックのコレクションにすぎないという見出しの考えは間違っています。通常、ロックフリー構造で再試行ループが発生しますが、動作は大きく異なります。ロック(スピンロックを含む)は一部のリソースを排他的に取得し、他のスレッドは保持されている間は進行できません。その意味での「再試行」は、排他的なリソースが解放されるのを単に待っているだけです。
BeeOnRope 2017

1
一方、ロックフリーアルゴリズムは、CASやその他のアトミック命令を使用して排他的なリソースを取得するのではなく、何らかの操作を完了します。それらが失敗した場合、それは別のスレッドとの時間的にきめ細かい競合が原因であり、その場合、他のスレッドは進行しました(操作を完了しました)。スレッドが無期限に疑わしい場合でも、他のすべてのスレッドは進行する可能性があります。これは、定性的にもパフォーマンス的にも、排他的ロックとは大きく異なります。「再試行」の数が最も...でも重い競合下のCASは、ループのために、通常は非常に低い
BeeOnRope

1
...しかし、もちろんそれは適切なスケーリングを意味するものではありません。CASの障害の数が低。
beeOnRope 2017

1
@ AndrasVass-「良い」コードと「悪い」ロックフリーコードにも依存すると思います。確かに、誰でも構造を記述してロックフリーと呼ぶことができますが、実際にはユーザーモードのスピンロックを使用しているだけで、定義を満たしていません。また、興味のある読者には、ロックベースおよびロックフリーアルゴリズムのさまざまなカテゴリを正式に調べたHerlihy andShavitのこのペーパーをチェックすることをお勧めします。このトピックに関するHerlihyの記事も読むことをお勧めします。
beeOnRope 2017

1
@ AndrasVass-同意しません。古典的なロックフリー構造(リスト、キュー、並行マップなど)のほとんどは、共有された可変構造に対しても回転せず、たとえばJavaでの同じものの実際の既存の実装は、同じパターンに従います(私はそうではありません)ネイティブコンパイルされたCまたはC ++で利用できるものに精通しており、ガベージコレクションがないため、そこでは困難です)。おそらく、あなたと私はスピニングの定義が異なります。ロックフリーのものの「スピニング」に見られる「CAS再試行」は考慮していません。IMOの「回転」は、ホット待機を意味します。
BeeOnRope 2017

27

ジョー・ダフィの本:

http://www.bluebytesoftware.com/books/winconc/winconc_book_resources.html

彼はまた、これらのトピックに関するブログを書いています。

ローロックプログラムを正しく実行するための秘訣は、ハードウェア、オペレーティングシステム、およびランタイム環境の特定の組み合わせでのメモリモデルのルールを正確に理解することです。

私は個人的に、InterlockedIncrementを超えて正しいローロックプログラミングを行うのに十分なほど賢いわけではありませんが、あなたが素晴らしいなら、それを選んでください。コードにたくさんのドキュメントを残して、メモリモデルの不変条件の1つを誤って壊して、見つけられないバグを導入しないように注意してください。


38
だから、両方の場合はエリックリペットジョンスキートはロックフリーのプログラミングは賢く自分より人のためだけだと思うし、私は謙虚に、すぐにアイデアから叫んで逃げます。;-)
dodgy_coder 2012

20

最近は「ロックフリースレッド」のようなものはありません。コンピュータハードウェアが遅くて高価だった前世紀の終わりに、それは学界などにとって興味深い遊び場でした。 デッカーのアルゴリズムは常に私のお気に入りでした。最新のハードウェアはそれを放牧しました。それはもう動作しません。

2つの開発がこれを終わらせました:RAMとCPUの速度の間の拡大する格差。また、チップに複数のCPUコアを搭載するチップメーカーの能力。

RAM速度の問題により、チップ設計者はCPUチップにバッファを配置する必要がありました。バッファにはコードとデータが格納されており、CPUコアからすばやくアクセスできます。また、RAMとの間ではるかに遅い速度で読み書きできます。このバッファはCPUキャッシュと呼ばれ、ほとんどのCPUには少なくとも2つあります。第1レベルのキャッシュは小さくて高速で、第2レベルのキャッシュは大きくて低速です。CPUが第1レベルのキャッシュからデータと命令を読み取ることができる限り、CPUは高速で実行されます。キャッシュミスは非常にコストがかかります。データが1番目のキャッシュにない場合は最大10サイクル、2番目のキャッシュにない場合は最大200サイクル、CPUをスリープ状態にします。羊。

すべてのCPUコアには独自のキャッシュがあり、RAMの独自の「ビュー」を格納します。CPUがデータを書き込むと、書き込みはキャッシュに行われ、キャッシュはゆっくりとRAMにフラッシュされます。必然的に、各コアはRAMの内容について異なるビューを持つようになります。つまり、あるCPUは、そのRAM書き込みサイクルが完了てCPUが自身のビューを更新するまで、別のCPUが何を書き込んだかを知りません。

これは、スレッド化とは劇的に互換性がありません。あなたはいつも本当にあなたが別のスレッドによって書き込まれたデータを読み込む必要があるときに別のスレッドの状態が何であるかを気に。これを確実にするには、いわゆるメモリバリアを明示的にプログラムする必要があります。これは、すべてのCPUキャッシュが一貫した状態にあり、RAMの最新のビューを持つことを保証する低レベルのCPUプリミティブです。保留中の書き込みはすべてRAMにフラッシュする必要があり、キャッシュを更新する必要があります。

これは.NETで利用可能であり、Thread.MemoryBarrier()メソッドが実装します。これがlockステートメントが実行するジョブの90%(および実行時間の95%以上)であることを考えると、.NETが提供するツールを回避し、独自のツールを実装しようとすることで、先に進むことはできません。


2
@ Davy8:構成はそれをまだ難しくします。2つのロックフリーハッシュテーブルがあり、コンシューマーとして両方にアクセスする場合、これは全体としての状態の一貫性を保証するものではありません。今日来ることができる最も近いものは、たとえば1つのatomicブロックに2つのアクセスを配置できるSTMです。全体として、ロックフリー構造を使用することは、多くの場合、同じように注意が必要です。
Andras Vass 2010年

4
私は間違っているかもしれませんが、キャッシュコヒーレンシがどのように機能するかについて誤って説明したと思います。最新のマルチコアプロセッサのほとんどはコヒーレントキャッシュを備えています。つまり、キャッシュハードウェアは、対応するすべての「書き込み」呼び出しが完了するまで「読み取り」呼び出しをブロックすることにより、すべてのプロセスがRAMコンテンツの同じビューを持つようにします。Thread.MemoryBarrier()のドキュメント(msdn.microsoft.com/en-us/library/…)は、キャッシュの動作については何も述べていません。これは、プロセッサが読み取りと書き込みを並べ替えることを防ぐ単なるディレクティブです。
ブルックスモーセ

7
「最近は「ロックフリースレッド」のようなものはありません。」それをErlangとHaskellのプログラマーに伝えてください。
ジュリエット

4
@HansPassant:「最近は「ロックフリースレッド」のようなものはありません」。F#、Erlang、Haskell、Cilk、OCaml、MicrosoftのTask Parallel Library(TPL)、IntelのThreaded Building Blocks(TBB)はすべて、ロックフリーのマルチスレッドプログラミングを奨励しています。最近、本番コードでロックを使用することはめったにありません。
JD

5
@HansPassant:「いわゆるメモリバリア。これは低レベルのCPUプリミティブであり、すべてのCPUキャッシュが一貫した状態にあり、RAMの最新のビューを持っていることを保証します。保留中のすべての書き込みはRAMにフラッシュする必要があります。その後、キャッシュを更新する必要があります。」このコンテキストでのメモリバリアは、メモリ命令(ロードとストア)がコンパイラまたはCPUによって並べ替えられるのを防ぎます。CPUキャッシュの一貫性とは何の関係もありません。
JD


0

マルチスレッドに関しては、自分が何をしているのかを正確に知る必要があります。つまり、マルチスレッド環境で作業しているときに発生する可能性のあるすべてのシナリオ/ケースを調査することを意味します。ロックフリーマルチスレッドは、私たちが組み込むライブラリやクラスではなく、スレッドの旅で得た知識/経験です。


ロックフリーのスレッドセマンティクスを提供するライブラリは多数あります。STMは特に興味深いものであり、その周りにはかなりの数の実装があります。
マルセロカントス2010年

これの両面が見えます。ロックフリーライブラリから効果的なパフォーマンスを引き出すには、メモリモデルに関する深い知識が必要です。しかし、その知識を持っていないプログラマーでも、正確さの利点を活用できます。
Ben Voigt 2010年

0

.NETではロックフリースレッド化が難しい場合がありますが、ロックする必要があるものを正確に調査し、ロックされたセクションを最小化することで、ロックを使用するときに大幅な改善を行うことができます...これはロックの粒度の最小化とも呼ばれますます。

例として、コレクションスレッドを安全にする必要があるとだけ言ってください。各アイテムに対してCPUを集中的に使用するタスクを実行する場合は、コレクションを反復処理するメソッドの周りに盲目的にロックをかけないでください。あなたは可能性があるだけで、コレクションの浅いコピーを作成する周りのロックを配置する必要があります。コピーを反復処理すると、ロックなしで機能する可能性があります。もちろん、これはコードの詳細に大きく依存しますが、このアプローチでロックコンボイの問題を修正することができました。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.