現在の「ロックフリー」の実装は、ほとんどの場合、同じパターンに従います。
- いくつかの状態を読み取り、そのコピーを作成します*
- 修正コピー*
- 連動操作をする
- 失敗した場合は再試行
(*オプション: データ構造/アルゴリズムに依存)
最後のビットは、不気味なほどスピンロックに似ています。実際、これは基本的なスピンロックです。:)
これについては @nobugz に同意します。ロックフリー マルチスレッドで使用されるインターロック操作のコストは、実行する必要があるキャッシュとメモリ コヒーレンシ タスクによって支配されます。
ただし、「ロックフリー」のデータ構造で得られるのは、「ロック」が非常に細かいことです。これにより、2 つの同時スレッドが同じ「ロック」(メモリ位置) にアクセスする可能性が減少します。
ほとんどの場合の秘訣は、専用のロックを持たないことです。代わりに、たとえば、配列内のすべての要素またはリンクされたリスト内のすべてのノードを「スピンロック」として扱います。最後に読んでから更新がない場合は、読んで、変更して、更新しようとします。あった場合は、再試行します。
これにより、追加のメモリやリソースの要件を導入することなく、「ロック」(申し訳ありませんが、ロックなし:) が非常にきめ細かくなります。
よりきめ細かくすると、待機の可能性が減少します。追加のリソース要件を導入することなく、可能な限りきめ細かくすることは素晴らしいことだと思いませんか?
ただし、楽しみのほとんどは、正しいロード/ストア順序を確保することから得られます。
直感に反して、CPU はメモリの読み取り/書き込みを自由に並べ替えることができます。ちなみに、CPU は非常にスマートです。単一のスレッドからこれを観察するのは難しいでしょう。ただし、複数のコアでマルチスレッドを開始すると、問題が発生します。直観は崩壊します。命令がコードの前にあるからといって、それが実際に早く起こるとは限りません。CPU は順不同で命令を処理できます。特に、メイン メモリのレイテンシを隠してキャッシュをより有効に利用するために、メモリ アクセスを伴う命令に対してこれを行うことを好みます。
さて、直感に反して、コードのシーケンスが「トップダウン」に流れるのではなく、シーケンスがまったくないかのように実行され、「悪魔の遊び場」と呼ばれることがあります。どのようなロード/ストアの再注文が行われるかについて、正確な答えを出すことは不可能だと思います。代わりに、常に5月と5月と缶の観点から話し、最悪の事態に備えます。「ああ、CPUはこの読み取りをその書き込みの前に並べ替える可能性があるため、ここ、この場所にメモリ バリアを配置するのが最善です。」
これらの能力でさえ、CPU アーキテクチャ間で異なる可能性があるという事実によって、問題は複雑になります。たとえば、あるアーキテクチャで は発生しないことが保証されていることが、別のアーキテクチャでは発生する可能性があります。
「ロックのない」マルチスレッドを正しく行うには、メモリ モデルを理解する必要があります。
ただし、この記事で示されているように、メモリ モデルと保証を正しく取得することは簡単ではありません。Intel と AMD は、MFENCE
JVM 開発者の間で騒ぎを引き起こしたというドキュメントにいくつかの修正を加えました。結局のところ、開発者が最初から信頼していたドキュメントは、そもそもそれほど正確ではありませんでした。
.NET のロックは暗黙的なメモリ バリアをもたらすため、安全に使用できます (ほとんどの場合、つまり... たとえば、このJoe Duffy - Brad Abrams - Vance Morrisonの遅延初期化、ロック、揮発性およびメモリに関する偉大さを参照してください)。 :) (必ずそのページのリンクをたどってください。)
追加のボーナスとして、サイド クエストで .NET メモリ モデルを紹介します。:)
また、Vance Morrison の「oldie but goldie」: What Every Dev Must Know About Multithreaded Appsもあります。
...そしてもちろん、@Ericが述べたように、Joe Duffyはこの件に関する決定的な読み物です。
優れた STM は、可能な限りきめ細かなロックに近づけることができ、おそらく手作りの実装に近い、または同等のパフォーマンスを提供します。それらの 1 つは、MSのDevLabsプロジェクトのSTM.NETです。
あなたが .NET のみの熱狂者ではない場合、Doug Lea は JSR-166 で素晴らしい仕事をしました。
Cliff Clickは、Java や .NET の同時ハッシュ テーブルのようにロック ストライピングに依存しない興味深いハッシュ テーブルを採用しており、750 CPU まで十分に拡張できるようです。
Linux の領域に足を踏み入れることを恐れていない場合は、次の記事で、現在のメモリ アーキテクチャの内部構造と、キャッシュ ラインの共有がパフォーマンスをどのように損なうかについての洞察を提供します:すべてのプログラマがメモリについて知っておくべきこと.
@Ben は MPI について多くのコメントをしました。MPI ベースのソリューションは、スマートにしようとする中途半端なロックの実装よりも、推論が容易で、実装が容易で、エラーが発生しにくい可能性があります。(ただし、主観的には、STM ベースのソリューションにも当てはまります。) また、多くの成功例が示唆するように、Erlang などで適切な分散アプリケーションを正しく作成する方が何光年も簡単だと思います。
ただし、MPI には、単一のマルチコア システムで実行する場合のコストと問題があります。たとえば、Erlang では、プロセス スケジューリングとメッセージ キューの同期に関して解決すべき問題があります。
また、MPI システムは通常、"軽量プロセス" のために一種の協調的なN:M スケジューリングを実装します。これは、たとえば、軽量プロセス間で避けられないコンテキストの切り替えがあることを意味します。確かに、これは「従来のコンテキスト スイッチ」ではなく、ほとんどがユーザー空間操作であり、高速化できますが、インターロック操作にかかる 20 ~ 200 サイクル以下にできるとは思えません。ユーザーモードのコンテキスト切り替えは確かに遅いIntel McRT ライブラリーでも。軽量プロセスによる N:M スケジューリングは新しいものではありません。LWP は Solaris に長い間存在していました。彼らは見捨てられました。NTには繊維があった。それらは現在、ほとんどが遺物です。NetBSD には「アクティベーション」がありました。彼らは見捨てられました。Linux は、N:M スレッド化というテーマについて独自の見解を持っていました。今では少し死んでいるようです。
時々、新しい候補があります。たとえば、Intelの McRT や、最近では MicrosoftのConCRTと組み合わせたUser-Mode Schedulingなどです。
最低レベルでは、N:M MPI スケジューラと同じことを行います。Erlang (または任意の MPI システム) は、新しいUMSを活用することで、SMP システムで大きな利益を得る可能性があります。
OPの質問は、ソリューションのメリットや主観的な議論に関するものではないと思いますが、それに答えなければならない場合は、タスクに依存すると思います。多くのコアを備えた単一システム、ローロック/「ロックフリー」技術または STM のいずれかが、パフォーマンスの点で最良の結果をもたらし、上記のしわが解消されたとしても、パフォーマンスの点で常に MPI ソリューションを打ち負かすでしょう。例えばErlangで。
単一のシステムで動作するやや複雑なものを構築する場合は、従来の粗粒度ロックを選択するか、パフォーマンスが重要な場合は STM を選択します。
分散システムを構築する場合、MPI システムはおそらく自然な選択になるでしょう。
.NET用のMPI 実装
もあることに注意してください(ただし、それほどアクティブではないようです)。