ロックフリヌのデヌタ構造。 内偎。 RCU


この蚘事では、 libcdsラむブラリヌを広告するできれば邪魔にならないようにこずで、ロックフリヌのコンテナヌを䜜成する手法を玹介したす。

ロックフリヌコンテナの安党なメモリ解攟のための別の手法、RCUに぀いお説明したす。 この手法は、前述のアラハザヌドポむンタヌアルゎリズムずは倧きく異なりたす。

読み取り-コピヌ曎新RCUは、「ほが読み取り専甚」、぀たりめったに倉曎されないデヌタ構造甚に蚭蚈された同期手法です。 このような構造の兞型的な䟋は、マップずセットです。これらのほずんどの操䜜は怜玢、぀たりデヌタの読み取りです。 通垞のマップでは、呌び出される操䜜の90以䞊がキヌ怜玢であるず考えられおいるため、怜玢操䜜が最速であるこずが重芁です。 怜玢の同期は基本的に䞍芁です-ラむタヌのないリヌダヌは䞊行しお䜜業できたす。 RCUは、読み取り操䜜に察しおのみオヌバヌヘッドを最小限に抑えたす。

Read-Copy Updateずいう名前はどこから来たのですか 圓初、このアむデアは非垞にシンプルでした。デヌタ構造はめったに倉曎されたせん。 倉曎する必芁がある堎合は、 コピヌを䜜成し、 コピヌで倉曎デヌタの远加たたは削陀を行いたす。 同時に、パラレルリヌダヌは元の倉曎されおいない構造で動䜜したす。 ある安党な時点で、読者がいない堎合、デヌタ構造を倉曎されたコピヌで眮き換えるこずができたす。 その結果、以降のすべおのリヌダヌには、ラむタヌによっお行われた倉曎が衚瀺されたす。


RCUテクノロゞヌの䜜成者であり積極的なプロモヌタヌはPaul McKenneyです。 圌は「RCU愛奜家」の孊校党䜓を率いおおり、そこからロックフリヌおよび非䌝統的な同期スキヌムの分野で倚くの有名な科孊者が出おきたした。たた、圌はLinuxカヌネルLinuxカヌネル。


RCUは2002幎にLinuxカヌネルに導入されお以来、カヌネルコヌドにたすたす成長しおいたす。右の図を参照しおください。 長い間、それはオペレヌティングシステムのカヌネル専甚の同期技術ずしお䜍眮づけられおいたした。 カヌネルはナヌザヌずシステムの䞡方のすべおのスレッドを完党に制埡するため、カヌネル内でデヌタが倉曎されたコピヌで眮き換えられる安党な瞬間を刀断するのは非垞に簡単です。 しかし、私たちはRCUの応甚に興味がありたす、それは可胜ですか この質問に答える前に、RCUの理論ずそれに䜿甚される甚語をより詳现に怜蚎したす。

RCUの䞀般的な説明



RCUの抂念に関する䞊蚘の説明は非垞に単玔です。 ご存知のように、アトミック操䜜があるため、デヌタのコピヌを䜜成するこずはできたせんが、読み取りず䞊行しおデヌタ構造を「オンザフラむ」で倉曎したす。 次に、「リヌダヌ」は、デヌタ構造から芁玠を削陀する以倖の操䜜を実行するスレッドになりたす。 ラむタヌは、構造から䜕かを削陀するストリヌムです。 削陀されたデヌタを誰も「螏たない」ずきに削陀を行う必芁がありたす。そうしないず、ABAの問題からメモリの砎損たで、怜出が困難な問題が倚数発生したす。 RCUは、前述のハザヌドポむンタヌスキヌムずは異なる方法を䜿甚しお、これらすべおの問題を解決したす。

RCUテクニックの読者は、読み取り偎のクリティカルセクションで実行したす。 このようなクリティカルセクションに入るず、リヌダヌはrcu_read_lock()関数を呌び出し、 rcu_read_lock()するずrcu_read_lock()呌び出したす。 これらは非垞に軜量な機胜であり、パフォヌマンスにはほずんど圱響したせん。 Linuxカヌネルでは、たったく重量を量りたせんれロオヌバヌヘッド。
ストリヌムがクリティカル読み取りセクションにない堎合、ストリヌムは静止状態 静止状態、静止状態にあるず蚀われたす。 各スレッドが少なくずも1回は静止状態にある期間は、 猶予期間ず呌ばれたす。 猶予期間が終了する前に開始する各重芁な読み取りセクションは、猶予期間が終了する前に終了する必芁がありたす。 重芁な読み取りセクションは有限であるため、各猶予期間は有限であるこずが保蚌されおいたすスレッドの数は有限であり、優れたプログラマヌであり、無限ルヌプやスレッドクラッシュを回避できるこずが理解されおいたす。


デヌタ構造から芁玠を削陀するラむタヌスレッドは、構造から芁玠を陀倖し、猶予期間の終了を埅ちたす。 猶予期間の終了ずは、削陀する芁玠にアクセスできるリヌダヌがいないこずを意味したす図を参照、その「読み取り」長方圢は重芁な読み取りセクションです。 したがっお、ラむタヌスレッドはアむテムを安党に物理的に削陀できたす。
削陀は2段階で実行されたす。最初の段階である「削陀」は、デヌタ構造から芁玠をアトミックに削陀したすが、メモリを物理的に解攟したせん。 代わりに、ラむタヌは特別なsynchronize_rcu()プリミティブを呌び出しお猶予期間の開始を通知し、終了するのを埅ちたす。 削陀されたアむテムには、ラむタヌず䞊行しお重芁な読曞セクションを宣蚀したリヌダヌのみがアクセスできたす図では、そのようなセクションは灰色で匷調衚瀺されおいたす。 定矩により、このような読者はすべお、猶予期間が終了する前に䜜業を終了したす。 猶予期間の終了時、぀たり、猶予期間䞭に開始たたはアクティブになったすべおの重芁な読み取りセクションが完了するず、削陀の2番目の段階である「レクラメヌション」、぀たり芁玠の䞋のメモリの物理的な削陀が開始されたす。

ご芧のずおり、RCUの同期手法は非垞に簡単です。 問題は残っおいたす-ナヌザヌコヌドで猶予期間の終了を刀断する方法は 元のRCUは、すべおのスレッドを完党に制埡できるため、Linuxカヌネルに合わせお倧幅に調敎されおいたす。 ナヌザヌ空間コヌドの堎合、元のRCUのアプロヌチは適甚できたせん。

ナヌザヌスペヌスRCU


この決定は、2009幎にP. McKenneyの代衚であるM.Desnoyersの論文 第6章RCUのUser-Level Implementationsで䞎えられたした。
M.Desnoyersは、ナヌザヌスペヌスRCUURCUの3぀の゜リュヌションを提䟛しおいたす。


汎甚URCU



M.Desnoyersは、URCUアルゎリズムを非垞に詳现か぀培底的に解析するため、いく぀かの倉数ず関数の名前のみを倉曎しおlibcdsで採甚されおいるものに察応するようにしたす。

URCUスキヌマは2぀の倉数を定矩したす。
 std::atomic<uint32_t> g_nGlobalCtl(1) ; struct thread_record { std::atomic<uint32_t> nThreadCtl; thread_record * pNext; thread_record(): nThreadCtl(0), pNext(nullptr) {} }; 

thread_record構造䜓には、スレッドにロヌカルなデヌタが含たれ、そのようなすべおのオブゞェクトをRCUスレッドのリストにリンクしたす。
nThreadCtlの䞋䜍31ビットには、URCU呌び出しのネストの深さのカりンタヌが含たれたすはい、URCUはクリティカルな読み取りセクションのほが無制限のネストを蚱可したす。高ビットは、スレッドがクリティカルな読み取りセクションに入る時点の猶予期間の識別子を決定したす。 説明したスキヌムでは、猶予期間の識別子は2぀だけで十分です。
グロヌバル倉数g_nGlobalCtlの䞊䜍ビットには珟圚の猶予期間の識別子が含たれ、䞋䜍ビットはnThreadCtlスレッドごずの倉数を初期化する圹割をnThreadCtl 、倉曎されたせん。
重芁な読み取りセクションから/に出入りするにはaccess_unlockそれぞれ関数access_lockおよびaccess_unlock䜿甚しaccess_lock 。
 static uint32_t const c_nControlBit = 0x80000000; static uint32_t const c_nNestMask = c_nControlBit — 1; void access_lock() { thread_record * pRec = get_thread_record(); assert( pRec != nullptr ); uint32_t tmp = pRec->nThreadCtl.load( std::memory_order_relaxed ); if ( (tmp & c_nNestMask) == 0 ) { pRec->nThreadCtl.store(g_nGlobalCtl.load( std::memory_order_relaxed ), std::memory_order_relaxed ); std::thread_fence( std::memory_order_acquire ); } else pRec->nThreadCtl.fetch_add( 1, std::memory_order_relaxed ); } void access_unlock() { thread_record * pRec = get_thread_record(); assert( pRec != nullptr ); pRec->nThreadCtl.fetch_sub( 1, std::memory_order_release ); } 

URCUのクリティカルセクションに入るず、呌び出しがネストされおいるかどうかがチェックされたす。 呌び出しがネストされおいる堎合぀たり、䞋䜍31ビットのカりンタヌがれロではない堎合、ネストカりンタヌは単玔にむンクリメントされたす。 呌び出しがネストされおいない堎合、珟圚のスレッドのnThreadCtl倉数にグロヌバル倉数g_nGlobalCtl倀が割り圓おられたす。 これは、クリティカルセクションが特定の猶予期間高ビットg_nGlobalCtl に入力されたこずを瀺し、䜎ビットg_nGlobalCtlナニットは珟圚のストリヌムのネストカりンタヌを初期化したす。 クリティカルセクションぞの最初の最も倖郚からの入り口では、メモリの獲埗バリアが適甚されたす。 これにより、埌続のコヌドがプロセッサたたはコンパむラによっおバリアの䞊流に移動「最適化」されないこずが保蚌されたす。 これにより、すべおのプロセッサに察するストリヌムの珟圚の猶予期間の可芖性が保蚌されたす-この順序に違反するず、URCUアルゎリズムが厩れたす。 ネストされたクリティカルセクションに入る堎合、珟圚の猶予期間高ビットは倉わらないため、バリアは必芁ありたせん。
クリティカルセクション access_unlock を終了するnThreadCtl珟圚のスレッドのnThreadCtlネストカりンタヌが単玔に枛分されたす。 アトミック操䜜のリリヌスセマンティクスが適甚されたす。 実際、ここでリリヌスバリアが必芁になるのは、最䞊䜍のクリティカルセクションを離れるずきネストカりンタヌを1から0に移動するずき、ネストされたクリティカルセクションを離れるずき、リラックスしたセマンティクスで十分です。 ネストカりンタヌが1から0に移行するず、「ストリヌムがRCUを䜿甚しなくなった」ずいうアナりンスメントが実際に発生するため、カりンタヌをリセットするずきのリリヌスバリアが必芁です。぀たり、猶予期間からの終了はURCUアルゎリズムにずっお重芁で、コンパむラヌたたはプロセッサヌによる順序違反アルゎリズムの動䜜䞍胜に぀ながりたす。 コヌド内の状況「0-not 0」を認識するには、条件付き遷移が必芁になりたす。これは、 access_unlock関数にパフォヌマンスを远加する可胜性が䜎く、 access_unlockクリティカルセクションを䜿甚する䞻なパタヌンはネストなしです。

ご芧のずおり、読者偎のコヌドは非垞に軜量です。 アトミックな読み取りず曞き蟌みおよびスレッドロヌカルデヌタが䜿甚されたす。 もちろん、これはれロオヌバヌヘッドではありたせんが、それでもミュヌテックスやCASよりもはるかに優れおいたす。

ラむタヌスレッドは、アむテムを物理的に削陀する前に、たず猶予期間が完了しおいるこずを確認する必芁がありたす。 猶予期間を終了する条件は、次の2぀のいずれかです。

これらの条件は、次の機胜によっおチェックされたす。
 bool check_grace_period( thread_record * pRec ) { uint32_t const v = pRec->nThreadCtl.load( std::memory_order_relaxed ); return (v & general_purpose_rcu::c_nNestMask) && ((( v ^ g_nGlobalCtl.load( std::memory_order_relaxed )) & ~c_nNestedMask )); } 

物理的な削陀の前に、ラむタヌは珟圚の猶予期間の終了を予期するsynchronize関数を呌び出したす。
 std::mutex g_Mutex ; void synchronize() { std::atomic_thread_fence( std::memory_order_acquire ); { cds::lock::scoped_lock<std::mutex> sl( g_Mutex ); flip_and_wait(); flip_and_wait(); } std::atomic_thread_fence( std::memory_order_release ); } 

ここで、 g_MutexはURCUアルゎリズムのグロヌバルミュヌテックスですはい、はいURCUは䟝然ずしお同期技術であるため、ミュヌテックスがないため、どこにもありたせん。 したがっお、1぀のラむタヌスレッドのみがsynchronize入るこずができたす。 RCUは「ほが読み取り専甚」のデヌタ甚に配眮されるこずを忘れないでください。したがっお、このミュヌテックスでは特別なクラッシュは想定されおいたせん。
ラむタヌは、 flip_and_wait関数を呌び出すこずにより、猶予期間の終了を予期したす。
 void flip_and_wait() { g_nGlobalCtl.fetch_xor( c_nControlBit, std::memory_order_seq_cst ); for (thread_record* pRec = g_ThreadList.head(std::memory_order_acquire); pRec!= nullptr; pRec = pRec->m_pNext ) { while ( check_grace_period( pRec )) { sleep( 10 ); //  10  CDS_COMPILER_RW_BARRIER ; } } } 

この関数は、猶予期間の識別子を倉曎したす。これは、アトミックなfetch_xorを䜿甚しお新しい猶予期間の開始を意味し、すべおのcheck_grace_periodスレッドがこの新しい猶予期間を完了するたで埅機したす fetch_xorを呌び出しcheck_grace_period 。 擬䌌コヌドでは、埅機は10ミリ秒の単玔なスリヌプです。実際のlibcdsコヌドでは、バックオフ戊略を蚭定するテンプレヌトパラメヌタヌが䜿甚されたす。

ラむタヌがflip_and_wait 2回呌び出すのflip_and_waitなぜですか 明確にするために、2぀のストリヌムAずBを䜿甚したこの䞀連のアクションを怜蚎しおflip_and_wait 。

ただし、スレッドAは、Bが猶予期間を倉曎する前に開始したクリティカルセクションにありたす。 URCUのセマンティクスぞの違反。最終的にはABAからメモリ砎損に至るたで、党䜓的な問題に぀ながりたす。 リコヌル synchronizeは、芁玠のメモリを物理的に削陀する前にラむタヌによっお呌び出されたす

flip_and_wait 2回、぀たり猶予期間の終了を2回埅機するこずで、スレッドの競合的な実行が原因である䞊蚘の問題を解決したす。
別の解決策
もちろん、猶予期間識別子ビットの代わりにカりンタを䜿甚する堎合、別の方法でこの問題を解決できたす。 しかし、タグ付きポむンタヌアルゎリズムに関する蚘事で既に芋た問題が発生したした。カりンタヌがオヌバヌフロヌしおいたす。 信頌性のために、カりンタは32ビットである必芁があり、そうすればオヌバヌフロヌは怖くありたせん。 ただし、このようなカりンタを䜿甚するず、32ビットプラットフォヌムで64ビットのアトミックタむプが必芁になりたす。 このタむプはそうでないか、むしろ非効率的です。 たたは、URCUの重芁なセクションのネストを攟棄する必芁がありたすが、これもあたり䟿利ではありたせん。
したがっお、猶予期間の識別子ずしおビットを䜿甚し、2぀のflip_and_waitを呌び出す䞀般的な゜リュヌションに぀いお説明したす。


libcdsでのURCU実装



䞊蚘のURCUアルゎリズムはすべおのナヌザヌに適しおいたすが、 各削陀の前にかなり重いsynchronizeを呌び出す必芁がありたす。 これを改善する方法はありたすか
はい、ハザヌドポむンタヌアルゎリズムず同じ方法を䜿甚しお、遅延削陀を適甚できたす。 削陀する代わりに、芁玠をバッファに入れたす。 バッファヌがいっぱいになった堎合にのみ、 synchronize関数を呌び出したす。 ハザヌドポむンタヌずは異なり、URCUでは、バッファヌはすべおのスレッドに共通です䞀般に、スレッドごずのバッファヌを䜜成できたすが、これを劚げるものは䜕もありたせん。
さらに、バッファが䞀杯になったずきにバッファをクリヌンアップするために共有したラむタの速床を䜎䞋させないために、バッファクリヌニング機胜、぀たり実際の削陀を別のスレッドに割り圓おるこずができたす。

libcdsラむブラリには5぀の URCU実装があり、それらはすべおcds::urcu 。


このような豊富なURCU実装は、URCUの䞋でコンテナの特殊化を蚘述する問題を匕き起こしたす。 実際、URCUスキヌムのコンテナの実装は、ハザヌドポむンタヌの実装ずは倧きく異なりたす。 したがっお、URCUには個別の専門化が必芁です。 5぀ではなく、1぀の専門分野が必芁です。
URCUでの専門分野の蚘述を容易にするために、ラッパヌクラスcds::urcu::gcが導入されたした。
 template <typename RCUimpl> class gc; 

ここで、 RCUimplはRCUimplの実装の1぀ですgeneral_instant 、 general_bufferedなど。このようなラッパヌがあるず、URCUの特殊化を簡単に蚘述できたす。
 template < class RCU, typename Key, typename Value, class Traits > class SplitListMap< cds::urcu::gc< RCU >, Key, Value, Traits > ... 


libcdsでは、削陀時のURCUアルゎリズムの䞻な機胜はsynchronizeではなく、 retire_ptr 。 この関数は、削陀されたアむテムをURCUバッファヌに配眮し、適切なタむミングバッファヌがいっぱいになったずきなどにsynchronize呌び出したす。 したがっお、 synchronizeするための明瀺的な呌び出しは必芁でsynchronizeたせんが、有効です。 さらに、この゜リュヌションはURCUずハザヌドポむンタヌのむンタヌフェヌスを統合したす。

リストされおいるすべおのURCUアルゎリズムは、libcdsの兞型的な方法で実装されたす。それぞれに぀いお、ラッパヌオブゞェクトコンストラクタヌcds::urcu::gc<cds::urcu::general_buffered<> >をmain()の先頭で呌び出すこずで初期化されるグロヌバルシングルトンオブゞェクトがありたす、 cds::Initialize()を呌び出した埌cds::Initialize() 
 #include <cds/init.h> //cds::Initialize  cds::Terminate #include <cds/gc/general_buffered.h> // general_buffered URCU int main(int argc, char** argv) { //  libcds cds::Initialize() ; { //  general_buffered URCU  cds::urcu::gc<cds::urcu::general_buffered<> > gbRCU ; //  main thread  lock-free  // main thread    //   libcds cds::threading::Manager::attachThread() ; // , libcds    //     ... } //  libcds cds::Terminate() ; } 


ハザヌドポむンタヌスキヌムず同様に、URCUコンテナヌを䜿甚する各スレッドは、特別な方法で初期化する必芁がありたす。
 // cds::threading::Manager #include <cds/threading/model.h> int myThreadEntryPoint(void *) { //     libcds cds::threading::Manager::attachThread() ; //        // lock-free  libcds ... //    libcds cds::threading::Manager::detachThread() ; return 0; } 


libcdsラむブラリのURCUコンテナの䜿甚は完党に透過的です。URCUgcでコンテナオブゞェクトを宣蚀するだけです。 URCUを䜿甚した䜜業の詳现はすべお、コンテナヌのURCU特殊化の䞭に隠されおいたす。 そのようなコンテナにアクセスする堎合、倖郚同期は必芁ありたせん。
UPDおっず
「倖郚同期は必芁ありたせん」 -私は少し興奮したした。
実際、いく぀かのURCUコンテナの䞀郚のメ゜ッドでは、最初にクリティカル読み取りセクションに入る必芁がありたす。 通垞、これらはコンテナアむテムを削陀取埗するためのメ゜ッドです。 URCUは、キヌで芋぀かった芁玠ぞのポむンタヌを返す機胜を提䟛できたす。 このような機䌚は、競合しないスレッドによっおい぀でも芁玠を削陀できるため、デスポむンタの戻り倀が通垞䌌おいるロックフリヌの䞖界ではたれな䟋倖です。 ただし、返された芁玠ポむンタを安党に䜿甚するには、重芁な読み取りセクションにいる必芁がありたす。 そのため、この堎合、コンテナメ゜ッドを呌び出す前にaccess_lockを明瀺的に呌び出す必芁があり、ポむンタヌaccess_unlockがaccess_unlock最良の䟋倖に察しお安党なメ゜ッドは、別のコヌドブロックでaccess_unlock -lockを䜿甚するこずです。
libcdsラむブラリのURCUコンテナの各メ゜ッドの説明には、このメ゜ッドを呌び出す方法が瀺されおいたす-クリティカルセクションかどうか。

libcdsからのURCUの実装に基づいお独自のコンテナクラスを䜜成する堎合は、ラむブラリのURCUコンテナの内郚デバむスを詳现に理解する必芁がありたす。 原則ずしお、 gc::access_unlock()なこずはありたせんメ゜ッドに入るずきにgc::access_lock()をgc::access_unlock() 、終了するずきにgc::access_unlock()をgc::access_unlock() ここでgcはURCU実装の1぀です。䟋倖の安党性のために、関数を呌び出す代わりにスコヌプロックテクニックを䜿甚するこずをお勧めしたす 。 唯䞀の埮劙な点は、芁玠の削陀ですdeleteメ゜ッドもクリティカル読み取りセクションにある必芁がありたすが、 gc::retire_ptr呌び出すこずによる芁玠の物理的な削陀はクリティカルセクションの倖偎で行う必芁がありたす。そうしないずデッドロックが発生したす gc::retire_ptrはsynchronizeを呌び出すこずができたす

Libcdsは、すべおのセットおよびマップクラスのURCU特殊化を定矩したす。 キュヌコンテナずスタックコンテナのURCUの特殊化は未定矩です。「ほが読み取り専甚」のコンテナではないため、URCUはそれらに察応しおいたせん。

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


All Articles