マルチスレッドで使用するために独自の「ロックフリー」コンテナを設計したという人々/記事/ SOの投稿を見てきました。彼らがパフォーマンスに影響を与えるモジュラストリックを使用していないと仮定すると(つまり、各スレッドはいくつかのモジュロに基づいてのみ挿入できます)、データ構造をマルチスレッド化してロックフリーにする方法は???
この質問は、CおよびC++を対象としています。
ロックフリープログラミングの鍵は、ハードウェア固有のatomic操作を使用することです。
実際のところ、ロック自体もこれらのアトミック操作を使用する必要があります!
ただし、ロックされたプログラミングとロックフリーのプログラミングの違いは、ロックフリープログラムが単一のスレッドによって完全に停止されることは決してないということです。対照的に、ロックプログラムで1つのスレッドがロックを取得してから無期限に中断すると、プログラム全体がブロックされ、進行できなくなります。対照的に、ロックのないプログラムは、個々のスレッドが無期限に中断されている場合でも、処理を進めることができます。
簡単な例を次に示します。同時カウンターの増分。どちらも「スレッドセーフ」、つまり複数回同時に呼び出すことができる2つのバージョンを提示します。最初にロックされたバージョン:
int counter = 0;
std::mutex counter_mutex;
void increment_with_lock()
{
std::lock_guard<std::mutex> _(counter_mutex);
++counter;
}
ロックフリーバージョン:
std::atomic<int> counter(0);
void increment_lockfree()
{
++counter;
}
ここで、何百ものスレッドがすべてincrement_*
関数を同時に呼び出すと想像してください。ロックされたバージョンでは、ロック保持スレッドがミューテックスをロック解除するまで、スレッドは進行できませんになります。対照的に、ロックフリーバージョンでは、すべてのスレッドが進行可能です。スレッドが保留されている場合、そのスレッドは作業を分担することはありませんが、他の全員が作業を続行できます。
一般的に、ロックフリープログラミングでは、予測可能なレイテンシに対してスループットと平均レイテンシスループットがトレードオフされることに注意してください。つまり、ロックのないプログラムは通常、対応するロックプログラムよりも少ない競合になります(アトミック操作は遅く、システムの残りの部分に影響を与えるため)。レイテンシが大きい。
ロックの場合、アイデアは、ロックを取得してから、他の誰も干渉できないことを確認して作業を行い、その後ロックを解放することです。
「ロックフリー」の場合、別の場所で作業を行い、この作業を「可視状態」にアトミックにコミットして、失敗した場合は再試行するという考え方です。
「ロックフリー」の問題は次のとおりです。
これらのことの組み合わせは、それが低い競合の下での比較的単純なものにのみ良いことを意味します。
研究者は、ロックフリーのリンクリスト(およびFIFO/FILOキュー)やロックフリーツリーなどを設計しました。それ以上複雑なものはないと思います。これらがどのように機能するかについては、難しいので複雑です。最も賢明なアプローチは、関心のあるデータ構造のタイプを判別し、そのデータ構造のロックフリーアルゴリズムに関する関連調査をWebで検索することです。
また、「ブロックフリー」と呼ばれるものがあることに注意してください。これは、常に作業をコミットでき、再試行する必要がないことを除いて、ロックフリーのようなものです。ブロックフリーのアルゴリズムを設計することはさらに困難ですが、競合は問題ではないため、ロックフリーに関する他の2つの問題はなくなります。 注:Kerrek SBの回答の「同時カウンター」の例は、ロックフリーではありませんが、実際にはブロックフリーです。
「ロックフリー」の考え方は、実際にはロックを持たないというものではなく、ほとんどの操作でロックを使用できないようにするいくつかの手法を使用して、ロックやクリティカルセクションの数を最小限に抑えることです。
optimistic design または transactional memory を使用して、すべての操作のデータをロックするのではなく、特定のポイントのみでトランザクションを実行できます(トランザクションメモリでトランザクションを実行する場合) 、または楽観的なデザインでロールバックする必要がある場合)。
他の代替手段は [〜#〜] cas [〜#〜] (比較してスワップ)などのいくつかのコマンドのアトミック実装に基づいており、 コンセンサス問題を解決することさえできます) それの実装が与えられています。参照でスワップを実行することで(そして、共通データで作業しているスレッドがないため)、CASメカニズムにより、ロックのない楽観的設計を簡単に実装できます(新しいデータに誰も変更していない場合に限り、新しいデータにスワップしますアトミックに行われます)。
ただし、これらのいずれかに基本的なメカニズムを実装するには、一部のロックが使用される可能性が最も高いが、データがロックされる時間の長さは(これらの手法が正しく使用されている場合は、最小限に抑えることが想定されています。
新しいCおよびC++標準(C11およびC++ 11)にはスレッドがあり、スレッド間でアトミックデータ型および操作間で共有されます。アトミック操作は、2つのスレッド間の競合に遭遇する操作を保証します。スレッドがそのような操作から戻ると、操作全体が確実に影響を受けるようになります。
そのようなアトミック操作に対する典型的なプロセッサーのサポートは、最新のプロセッサーで比較およびスワップ(CAS)またはアトミック増分のために存在します。
アトミックであることに加えて、データ型は「ロックフリー」プロパティを持つことができます。このプロパティは、そのような型の操作では、オブジェクトが割り込みハンドラーによって中断されたり、別のスレッドの読み取りが途中で中断されたりしても、中間状態にならないことを意味するため、おそらく「ステートレス」という造語である必要があります更新の。
いくつかのアトミックタイプはロックフリーである場合とそうでない場合があります。そのプロパティをテストするマクロがあります。ロックがないことが保証されているタイプは常に1つ、つまりatomic_flag
です。 `
windowsにインターロックされたライブラリがある http://msdn.Microsoft.com/en-us/library/windows/desktop/ms683590(v = vs.85).aspx これはロックフリーと見なされます。
*ilはmsdnから大きな専門家ではないと引用します:基本的に、ハードウェアが許可する場合、この「ロックフリー」を処理するアトミック操作が提供されます。それ以外の場合は、メモリを使用してロックが提供されます。バリーテクニック*
_InterlockedExchangeのいくつかのバリエーションは、それらが関係するデータ型と、プロセッサ固有の取得または解放のセマンティクスが使用されているかどうかによって異なります。 _InterlockedExchange関数は32ビット整数値で動作しますが、_InterlockedExchange64は64ビット整数値で動作します。 _InterlockedExchange_acqおよび_InterlockedExchange64_acq組み込み関数は、_acqサフィックスがない対応する関数と同じですが、操作が取得セマンティクスで実行される点が異なります。これは、クリティカルセクションに入るときに便利です。リリースセマンティクスを使用するこの関数のバージョンはありません。 Visual C++ 2005では、これらの関数は読み取り/書き込みメモリバリアとして動作します。詳細については、_ReadWriteBarrierを参照してください。これらのルーチンは組み込み関数としてのみ使用できます。