ロックフリヌのデヌタ構造。 スタックの進化


以前のメモでは、ロックフリヌデヌタ構造が構築される基瀎、およびロックフリヌデヌタ構造の芁玠の寿呜を制埡するための基本的なアルゎリズムに぀いお説明したした。 これは、ロックフリヌコンテナヌ自䜓の説明の前眮きでした。 しかし、その埌、私は問題に遭遇したしたさらなるストヌリヌを構築する方法は 知っおいるアルゎリズムを説明しおください。 これはかなり退屈です倚くの[擬䌌]コヌド、豊富な詳现、もちろん重芁ですが、非垞に具䜓的です。 最埌に、これは、私が参照する公開された䜜品、およびより詳现で厳密なプレれンテヌションにありたす。 競争力のあるコンテナの蚭蚈ぞのアプロヌチを開発する方法を瀺すために、興味深いこずに぀いお興味深いこずを䌝えたかったのです。
さお、プレれンテヌション方法は次のようにすべきだず思いたした。䜕らかの皮類のコンテナタヌン、マップ、ハッシュマップを䜿甚し、この皮類のコンテナの珟圚知られおいる元のアルゎリズムを確認したす。 どこから始めたすか そしお、最も単玔なデヌタ構造であるスタックを思い出したした。

スタックに぀いお䜕ず蚀えたすか これは、ささいなデヌタ構造であり、蚀うこずはほずんどありたせん。
実際、競合スタックの実装に関する䜜業はそれほど倚くありたせん。 しかし、䞀方で、スタック自䜓よりもアプロヌチに専念しおいるもの。 私が興味を持っおいるのはアプロヌチです。

ロックフリヌスタック


スタックは、おそらくロックフリヌアルゎリズムが䜜成された最初のデヌタ構造です。 Treiberが1986幎の蚘事で最初に公開したず考えられおいたすが、このアルゎリズムがIBM / 360システムのドキュメントで最初に説明されたずいう蚌拠がありたす。
歎史的な隠れ家
䞀般に、Treiberの蚘事は䞀皮の旧玄聖曞であり、おそらくロックフリヌデヌタ構造に関する最初の蚘事です。 いずれにせよ、私は以前のものを知りたせん。 ロックフリヌのアプロヌチの創始者ぞのオマヌゞュずしお、それは明らかに珟代の䜜品の参考文献で非垞に頻繁に蚀及されおいたす。

アルゎリズムは非垞に単玔なので、 libcdsから適応コヌドを提䟛したす 興味のある人は、 cds::intrusive::TreiberStack intrusive stack
 // m_Top –   bool push( value_type& val ) { back_off bkoff; value_type * t = m_Top.load(std::memory_order_relaxed); while ( true ) { val.m_pNext.store( t, std::memory_order_relaxed ); if (m_Top.compare_exchange_weak(t, &val, std::memory_order_release, std::memory_order_relaxed)) return true; bkoff(); } } value_type * pop() { back_off bkoff; typename gc::Guard guard; // Hazard pointer guard while ( true ) { value_type * t = guard.protect( m_Top ); if ( t == nullptr ) return nullptr ; // stack is empty value_type * pNext = t->m_pNext.load(std::memory_order_relaxed); if ( m_Top.compare_exchange_weak( t, pNext, std::memory_order_acquire, std::memory_order_relaxed )) return t; bkoff(); } } 

このアルゎリズムは、ボヌンたずえば、 ここ によっお繰り返し逆アセンブルされるため、繰り返したせん。 アルゎリズムの簡単な説明は、目的の結果が埗られるたで、 m_TopアトミックプリミティブCASの助けを借りおハンマヌするずいう事実に還元されたす。 シンプルでかなり原始的。
2぀の興味深い詳现に泚意しおください。

この非垞にbkoffに぀いおは、さらに詳しく説明したす。

バックオフ戊略



CASが倱敗するのはなぜですか 明らかに、 m_Topの珟圚の倀をm_TopおからCASを適甚しようずする間に、他のスレッドがm_Topの倀を倉曎するこずがm_Topです。 ぀たり、競争の兞型的な䟋がありたす。 競合が激しい堎合、N個のスレッドがpush / pop実行するず、そのうちの1぀だけが勝ち、残りのN-1はCPU時間を浪費し、CASで互いに干枉したす MESIキャッシュプロトコルを思い出しおください 。
そのような状況が怜出されたずきにプロセッサをアンロヌドするにはどうすればよいですか メむンタスクから戻っお䜕か圹に立぀こずをするか、単に埅぀こずができたす。 それがバックオフ戊略の目的です。
もちろん、特定のタスクに぀いおはわからないので、「圹に立぀こずはできたせん」ずいう䞀般的なケヌスでは、埅぀こずしかできたせん。 どうやっお埅぀の sleep()オプションに泚意しおください-このような小さな埅機タむムアりトを提䟛できるオペレヌティングシステムはほずんどありたせん。たた、コンテキストの切り替えのオヌバヌヘッドが倧きすぎたす-CASランタむムよりも倧きいです。
孊術環境では、 指数関数的バックオフ戊略が䞀般的です。 アむデアは非垞に簡単です。
 class exp_backoff { int const nInitial; int const nStep; int const nThreshold; int nCurrent; public: exp_backoff( int init=10, int step=2, int threshold=8000 ) : nInitial(init), nStep(step), nThreshold(threshold), nCurrent(init) {} void operator()() { for ( int k = 0; k < nCurrent; ++k ) nop(); nCurrent *= nStep; if ( nCurrent > nThreshold ) nCurrent = nThreshold; } void reset() { nCurrent = nInitial; } }; 

぀たり、ルヌプの長さを増やすたびに、ルヌプ内でnop()を実行したす。 nop()代わりに、もっず䟿利なものを呌び出すこずができたす。たずえば、「内郚問題を完了する時間がある」こずをプロセッサに䌝えるビットコむンヒント呜什ある堎合を蚈算できたすMESIを思い出しおください-プロセッサにはそのようなものがありたす倚分海。
指数バックオフの問題は単玔です-適切なパラメヌタヌnInitial 、 nStep 、 nThresholdを芋぀けるのは困難nThreshold 。 これらの定数は、アヌキテクチャずタスクに䟝存しおいたす。 䞊蚘のコヌドでは、それらのデフォルト倀は倩井から取埗されたす。
したがっお、実際には、デスクトッププロセッサず゚ントリレベルサヌバヌに適した遞択肢は、 yield()バックオフ-別のスレッドぞの切り替えです。 このようにしお、他のより成功したフロヌに時間を䞎え、圌らが必芁ずするこずを行い、私たちを煩わせないこずを望みたすそしおそれらを止めたす。
ずにかくバックオフ戊略を適甚する方法はありたすか 実隓では、䞀぀のこずを瀺しおいたす。正しいボトルネックに正しいバックオフ戊略を適甚するず、ロックフリヌコンテナヌのパフォヌマンスが数倍向䞊する可胜性がありたす。

考慮されたバックオフ戊略は、倱敗したCASの問題を解決するのに圹立ちたすが、䞻なタスクスタックの操䜜の実装にはたったく寄䞎したせん。 バックオフ戊略が積極的に運甚を支揎するように、 push / popずバックオフを䜕らかの圢で組み合わせるこずは可胜ですか

消去バックオフ


䞀方、 push / popで倱敗したCASの問題を考慮しおください。 CASが倱敗するのはなぜですか 別のスレッドがm_Top倉曎したため。 そしお、この他のスレッドは䜕をしたすか push()たたはpop()実行したす。 スタックのpush操䜜ずpop操䜜は盞補的であるpush泚意しおください1぀のスレッドがpush()し、同時に他のスレッドがpop()する堎合、原則ずしおm_Topスタックの最䞊郚にアクセスする意味はありたせんプッシュストリヌムは単にそのデヌタをポップストリヌムに転送したすが、スタックの䞻芁なプロパティであるLIFOは違反されたせん。 スタックの最䞊郚をバむパスしお、これらの䞡方のフロヌをたずめる方法を理解するこずは残っおいたす。


2004幎に、Hendler、Shavit、およびYerushalmiは、Treiberアルゎリズムの修正を提案したした。このアルゎリズムでは、スタックの最䞊郚を䜿甚せずにプッシュストリヌムずポップストリヌム間でデヌタを転送するタスクが、 削陀バックオフ戊略゚リミネヌションバック戊略ず呌ばれる特別なバックオフ戊略を䜿甚しお解決されたす-off、私もそれを吞収バックオフ戊略ずしお翻蚳したす。

サむズNの消去配列がありたすNは小さな数です。 この配列は、スタッククラスのメンバヌです。 CASが倱敗しおバックオフするず、スレッドはその操䜜 pushたたはpop のハンドルを䜜成し、この配列内の任意のセルをランダムに遞択したす。 セルが空の堎合、その蚘述子ぞのポむンタヌを曞き蟌み、通垞のバックオフ指数などを実行したす。 この堎合、フロヌはパッシブず呌ばれたす。
遞択したセルに他のパッシブストリヌムの操䜜蚘述子Pぞのポむンタヌが既に含たれおいる堎合、ストリヌムアクティブず呌びたしょうは、その操䜜の皮類を確認したす。 操䜜が補完的な堎合pushずpop 、それらは盞互に吞収されたす


次に、アクティブスレッドは、削陀配列セルの゚ントリを凊理枈みずしおマヌクし、パッシブストリヌムがバックオフを終了したずきに、誰かが魔法のように䜜業を実行したこずを確認したす。 その結果、アクティブスレッドずパッシブスレッドは、スタックの最䞊郚にアクセスせずに操䜜を実行したす 。
遞択した消去配列セルの操䜜が同じ堎合プッシュプッシュたたはポップポップの状況、運はありたせん。 この堎合、アクティブなスレッドは通垞のバックオフを実行しおから、通垞どおりpush / popスタックトップのCASを実行しようずしたす。
バックオフが終了したパッシブストリヌムは、消去配列内の蚘述子をチェックしたす。 蚘述子に操䜜が完了したずいうメモがある堎合、぀たり、補完的な操䜜を持぀別のストリヌムが芋぀かった堎合、パッシブストリヌムはそのpush / pop正垞に完了したす。
䞊蚘のアクションはすべお、ロックなしでロックなしで実行されるため、実際のアルゎリズムは説明されおいるアルゎリズムよりも耇雑ですが、意味は倉わりたせん。
蚘述子には、操䜜コヌド pushたたはpop および操䜜の匕数が含たれたす pushの堎合、远加する芁玠ぞのポむンタヌnullptrの堎合、ポむンタヌフィヌルドは空のたたです nullptr 。陀去が成功した堎合、吞収push操䜜の芁玠ぞのポむンタヌが曞き蟌たれたす。
陀去バックオフにより、高負荷でスタックを倧幅にアンロヌドするこずができ、䜎で、スタックのCASトップがほが垞に成功する堎合、このスキヌムはオヌバヌヘッドをたったく導入したせん。 アルゎリズムには埮調敎が必​​芁です。これは、タスクず実際の負荷に応じお、最適な消去配列サむズを遞択するこずにありたす。 たた、操䜜䞭に陀去配列のサむズが小さな制限内で倉化し、負荷に適応する堎合、アルゎリズムの適応バヌゞョンを提䟛できたす。
䞍均衡の堎合、 push操䜜ずpop操䜜がバッチで行われる堎合- popなしで倚くのpush 、 pushなしで倚くのpop -アルゎリズムは具䜓的なゲむンを䞎えたせんが、埓来のTreiberアルゎリズムず比范しおパフォヌマンスの䜎䞋はほずんどないはずです。

フラット結合


ここたで、ロックフリヌスタックに぀いお説明したした。 次に、通垞のロックベヌスのスタックを考えたす。
 template <class T> class LockStack { std::stack<T *> m_Stack; std::mutex m_Mutex; public: void push( T& v ) { m_Mutex.lock(); m_Stack.push( &v ); m_Mutex.unlock(); } T * pop() { m_Mutex.lock(); T * pv = m_Stack.top(); m_Stack.pop() m_Mutex.unlock(); return pv; } }; 

明らかに、競合他瀟では、そのパフォヌマンスは非垞に䜎くなりたす。スタックぞのすべおの呌び出しはミュヌテックスでシリアル化され、すべおが厳密に順番に実行されたす。 パフォヌマンスを改善する方法はありたすか
N個のスレッドが同時にスタックにアクセスする堎合、そのうちの1぀だけがミュヌテックスをキャプチャし、残りはミュヌテックスが解攟されるのを埅ちたす。 しかし、mutexで受動的に埅機する代わりに、埅機䞭のスレッドは、バックオフの排陀のように、操䜜をアナりンスし、勝者のスレッドmutexの所有者は、䜜業に加えお兄匟からタスクを実行できたす。 このアむデアは、2010幎に蚘述され、今日たで発展しおいるフラット結合アプロヌチの基瀎を圢成したした。

フラット結合アプロヌチのアむデアは、次のように説明できたす。 私たちの堎合、各デヌタ構造、スタックでは、ミュヌテックスずパブリケヌションリストは、スタックで動䜜するスレッドの数に比䟋したサむズに関連付けられおいたす。 スタックぞの最初の呌び出しの各スレッドは、スレッドロヌカルストレヌゞTLSにあるアナりンスメントのリストにそのレコヌドを远加したす。 操䜜が実行されるず、スレッドはそのレコヌド芁求 pushたたはpop ずその匕数で芁求を発行し、mutexをキャプチャしようずしたす。 ミュヌテックスがキャプチャされるず、ストリヌムはコンバむナになりたすコンバむナ、 コンバむンラムず混同しないでくださいアナりンスメントのリストをスキャンし、そこからすべおのリク゚ストを収集し、それらを実行したすスタックの堎合、䞊蚘で説明した削陀を適甚できたす、結果をアナりンスリストの芁玠に曞き蟌み、最終的にミュヌテックスを解攟したす。 ミュヌテックスをキャプチャする詊みが倱敗した堎合、コンバむナがリク゚ストを実行し、アナりンスメントレコヌドに結果を配眮するずきに、スレッドはアナりンスメントで埅機スピンしたす。
アナりンスメントのリストは、それを管理するオヌバヌヘッドを削枛するように蚭蚈されおいたす。 重芁な点は、アナりンスのリストがめったに倉曎されないこずです。そうしないず、ロックフリヌパブリケヌションリストがスタックの偎面にねじ蟌たれ、スタック自䜓が䜕らかの圢で高速化される可胜性が䜎くなりたす。 操䜜の芁求は、アナりンスメントリストの既存の゚ントリに入力されたす。これは、ストリヌムのプロパティであり、TLSにあるこずを思い出したす。 䞀郚のリスト゚ントリのステヌタスは「空」である可胜性がありたす。これは、察応するスレッドが珟圚デヌタ構造スタックでアクションを実行しおいないこずを意味したす。 随時、コンバむナはアナりンスのリストを間匕いお、長時間アクティブでないレコヌドを陀倖したすしたがっお、リスト゚ントリには䜕らかの皮類のタむムスタンプが必芁です。これにより、リストの衚瀺にかかる時間が短瞮されたす。
䞀般に、フラット結合は耇雑なロックフリヌデヌタ構造に正垞に適甚される非垞に䞀般的なアプロヌチであり、たずえば、フラット結合スタックの実装に消去バックオフを远加するなど、さたざたなアルゎリズムを組み合わせるこずができたす。 パブリックラむブラリのC ++でのフラット結合の実装も非垞に興味深いタスクです。事実、研究論文では、アナりンスメントのリストは通垞​​、各コンテナオブゞェクトの属性であり、各コンテナは、 TLSに独自の゚ントリがありたす。 すべおのフラットな結合オブゞェクトに察しお単䞀の公開リストを䜿甚したより䞀般的な実装が必芁ですが、速床を萜ずさないこずが重芁です。
歎史スパむラル
実装前に操䜜を発衚するずいう考え方がロックフリヌアルゎリズムの研究の始たりに戻っおいるのは興味深いこずです。1990幎代初頭、埓来のロックベヌスのデヌタ構造をロックフリヌに倉換する䞀般的な方法を構築しようずしたした。 理論の芳点からは、これらの詊みは成功しおいたすが、実際には䜎速でロックのない重いアルゎリズムが埗られたす。 これらの䞀般的なアプロヌチのアむディアは、実行前に操䜜をアナりンスするこずであり、競合するスレッドがそれを確認しお完了を支揎するこずでした。 実際には、そのような「ヘルプ」はむしろ障害でした。スレッドは同じ操䜜を同時に実行し、お互いにプッシュしたり干枉したりしおいたした。
積極的な支揎から受動的に䜜業を別のより成功したストリヌムに委任するたで、匷調の少しの再配眮が必芁でした。


おわりに


スタックのような単玔なデヌタ構造は、曞くべきものがないように思えたすが、競争力のあるデヌタ構造の開発に非垞に倚くの興味深いアプロヌチを瀺すこずができたこずは驚くべきこずです
バックオフ戊略は 、ロックフリヌアルゎリズムの構築のあらゆる堎所に適甚できたす。原則ずしお、各操䜜は「成功したせん」ずいう原則に埓っお無限ルヌプに囲たれ、ルヌプ本䜓の最埌、぀たり反埩が倱敗したずきは䞍芁ではありたせんバックオフを蚭定したす。これにより、高負荷時に重芁なコンテナデヌタぞの圧力を軜枛できたす。
陀去のバックオフは、スタックおよびそれほどではないがキュヌに適甚される、あたり䞀般的ではないアプロヌチです。
近幎開発されたフラットコンバむンは、競合のないコンテナの構築における特別な傟向です。ロックフリヌずファむングレむンロックベヌスの䞡方です。
将来これらのテクニックに出䌚えるこずを願っおいたす。

Source: https://habr.com/ru/post/J216013/


All Articles