このようなほとんど不可能なことを行うための鍵は、InterlockedCompareExchangeを使用することです。(これはWin32が使用する名前ですが、マルチスレッド対応のプラットフォームには、InterlockedCompareExchangeに相当するものがあります)。
アイデアは、構造のコピーを作成することです(これは、アトミック読み取りを実行するのに十分小さくなければなりません(64、または移植性を処理できる場合は、x86で128ビット)。
提案された更新で別のコピーを作成し、ロジックを実行してコピーを更新してから、InterlockedCompareExchangeを使用して「実際の」構造を更新します。InterlockedCompareExchangeが行うことは、値が状態更新前に開始した値であることをアトミックに確認し、それがまだその値である場合は、新しい状態で値をアトミックに更新します。通常、これは無限ループに包まれており、他の誰かが途中で値を変更しなくなるまで試行を続けます。大まかにパターンは次のとおりです。
union State
{
struct
{
short a;
short b;
};
uint32_t volatile rawState;
} state;
void DoSomething()
{
// Keep looping until nobody else changed it behind our back
for (;;)
{
state origState;
state newState;
// It's important that you only read the state once per try
origState.rawState = state.rawState;
// This must copy origState, NOT read the state again
newState.rawState = origState.rawState;
// Now you can do something impossible to do atomically...
// This example takes a lot of cycles, there is huge
// opportunity for another thread to come in and change
// it during this update
if (newState.b == 3 || newState.b % 6 != 0)
{
newState.a++;
}
// Now we atomically update the state,
// this ONLY changes state.rawState if it's still == origState.rawState
// In either case, InterlockedCompareExchange returns what value it has now
if (InterlockedCompareExchange(&state.rawState, newState.rawState,
origState.rawState) == origState.rawState)
return;
}
}
(上記のコードが実際にコンパイルされない場合はご容赦ください-頭のてっぺんから書きました)
素晴らしい。これで、ロックレスアルゴリズムを簡単に作成できます。間違い!問題は、アトミックに更新できるデータの量が大幅に制限されていることです。
一部のロックレスアルゴリズムは、並行スレッドを「支援」する手法を使用します。たとえば、複数のスレッドから更新できるようにしたいリンクリストがある場合、他のスレッドは、「最初」と「最後」のポインターが競合している場合に更新を実行して、それらが「last」が指すノードで、ノードの「next」ポインタはnullではありません。この例では、「最後の」ポインターが間違っていることに気付いたときに、インターロックされた比較交換を使用して、現在のノードをまだ指している場合にのみ、最後のポインターを更新します。
(スピンロックのように)「スピン」またはループするトラップに陥らないでください。「他の」スレッドが何かを終了することを期待しているため、簡単にスピンすることには価値がありますが、そうでない場合もあります。「その他」のスレッドはコンテキストスイッチされており、実行されていない可能性があります。あなたはただCPU時間を食べて、条件が真になるまで回転することによって電気を燃やしている(おそらくラップトップのバッテリーを殺している)。スピンを開始した瞬間に、ロックレスコードをチャックして、ロックを使用して記述した方がよいでしょう。ロックは無制限の回転よりも優れています。
難しいものからばかげたものに移行するために、他のアーキテクチャに取り掛かることができる混乱を考えてみてください。x86/ x64では一般的にかなり寛容ですが、他の「弱く順序付けられた」アーキテクチャに入ると、何かが起こる領域に入ります。意味がありません-メモリの更新はプログラムの順序では発生しないため、他のスレッドが実行していることについての精神的な推論はすべてウィンドウの外に出ます。(x86 / x64にも「ライトコンバイン」と呼ばれるメモリタイプがあり、ビデオメモリの更新時によく使用されますが、フェンスが必要な任意のメモリバッファハードウェアに使用できます)これらのアーキテクチャでは、「メモリフェンス」操作を使用して保証する必要がありますフェンスの前のすべての読み取り/書き込み/両方が(他のコアによって)グローバルに表示されること。書き込みフェンスは、フェンスの前の書き込みが、フェンスの後の書き込みの前にグローバルに表示されることを保証します。読み取りフェンスは、フェンスの後の読み取りがフェンスの前で投機的に実行されないことを保証します。読み取り/書き込みフェンス(別名フルフェンスまたはメモリフェンス)は、両方の保証を行います。柵は非常に高価です。(「フェンス」の代わりに「バリア」という用語を使用する人もいます)
私の提案は、最初にロック/条件変数を使用して実装することです。それを完全に機能させるのに問題がある場合は、ロックレス実装を試みるのは絶望的です。そして、常に測定し、測定し、測定します。ロックを使用した実装のパフォーマンスは完全に良好であることがわかるでしょう。重要な顧客に対してデモを行っているときにのみ表示される、厄介なハングバグを伴う不安定なロックレス実装の不確実性はありません。おそらく、元の問題をより簡単に解決できるものに再定義することで問題を修正できます。おそらく、より大きなアイテム(またはアイテムのバッチ)がコレクションに入るように作業を再構築することで、全体へのプレッシャーを軽減できます。
ロックレス並行アルゴリズムを作成することは非常に困難です(他の場所で1000倍作成されているのを見たことがあると思います)。多くの場合、努力する価値もありません。