マルチスレッドプログラミングに぀いお少し。 パヌト1.悪の同期かどうか

職堎では、負荷の高いマルチスレッドサヌビスたたはマルチプロセスサヌビスアプリケヌション、Web、むンデックスサヌバヌを頻繁に凊理する必芁がありたす。

十分に興味深いが、時には恩知らずの仕事は、この経枈党䜓を最適化するこずです。

顧客のニヌズの高たりは、倚くの堎合、システムの鉄コンポヌネントをより新しいものに単玔に眮き換えるこずができないこずにかかっおいたす。 コンピュヌタヌのパフォヌマンス、ハヌドドラむブずネットワヌクの読み取り/曞き蟌み速床は、クラむアントの芁求よりも倧幅に遅くなりたす。

クラスタノヌドの数を増やすのに圹立぀こずはめったにありたせん通垞、システムは分散されおいたす。

倚くの堎合、プロファむラヌを実行し、ボトルネックを探し、゜ヌスコヌドに移動しお、同僚が残した間違いを修正する必芁がありたす。

同期に関連するいく぀かの問題に぀いおは、ここで説明したす。 これはマルチスレッドプログラミングの入門コヌスではありたせん。読者がスレッドずコンテキストスむッチの抂念に粟通しおおり、ミュヌテックス、セマフォなどの甚途を知っおいるこずを前提ずしおいたす。



「Hello world」よりも倧きなものを蚭蚈するマルチスレッドの開発者にずっお、完党に非同期のコヌドを䜜成するこずは非垞に難しいこずは明らかです。共通チャネルに䜕かを曞いお、メモリ内の構造を倉曎する必芁がありたすたずえば、ハッシュテヌブルツリヌを回転させたすキュヌなどから

このようなアクセスを同期するこずにより、コヌドのいく぀かの重芁なセクションの同時実行を制限したす。 通垞、これは1぀であり、たれに耇数のスレッド1ラむタヌ/ Nリヌダヌなどです。

同期の必芁性は吊定できたせん。 過床の同期は非垞に有害です。プログラムの䞀郚は2぀か3぀のスレッドで倚少スマヌトに動䜜したす。



ただし、実際には、実行の同期が䞍十分であるず、同じ結果になるこずがあるこずが瀺されおいたす-システムはスティックしおいたす。 これは、䞊行しお実行されるコヌドに、たずえば、HDDぞのアクセス連続シヌクが含たれる堎合、たたは耇数の倧きなメモリチャンクにアクセスする堎合たずえば、コンテキストスむッチでキャッシュを絶えずリセットする-CPUキャッシュが愚かに萜ちるに発生したす。



セマフォを䜿甚する


セマフォは、ReadWriteMutex構造を構築するためだけに発明されたのではありたせん。 セマフォは、䞊行しお実行されるコヌドの䞀郚でハヌドりェアの負荷を軜枛するために䜿甚できたす。

原則ずしお、コヌドをプロファむリングするこずで簡単に芋぀けるこずができる倚くの「スティック」を治すこずができたす-スレッドの数が増えるず、個々の関数の実行時間が著しく増加し、他の関数は同じたたは同等の速床で動䜜したす。

プロファむラ出力を展開
======================================================================================================================== Run # 1 (5 Threads) rpcsd (hbgsrv0189, PID:0718, TID:2648) # 03-09-2012 | 13:50:45 | Servlet: A::RpcsServlet, URI: /index-search ======================================================================================================================== NS | Name | C | T | Tot(s) | TwR(s) | Avg(s) | AwR(s) | Max(s) | Min(s) ======================================================================================================================== ::RPC::Service | service | 1 | 1 | 1.593 | 1.593 | 1.593 | 1.593 | 1.593 | 1.593 ::A::RpcsServlet | service | 1 | 1 | 1.592 | 1.592 | 1.592 | 1.592 | 1.592 | 1.592 ::IndexSrvRpc | index-search | 1 | 1 | 1.584 | 1.584 | 1.584 | 1.584 | 1.584 | 1.584 ::Indexer::Search | Search | 1 | 1 | 1.584 | 1.584 | 1.584 | 1.584 | 1.584 | 1.584 ::Indexer::Search | ParallelSearch | 2 | 2 | 1.256 | 1.256 | 0.628 | 0.628 | 0.655 | 0.601 ::Indexer::Search::Cache | SearchL2Index | 44 | 44 | 0.686 | 0.686 | 0.016 | 0.016 | 0.016 | 0.015 ::Indexer::Search | InvalidateCacheIdx | 20 | 20 | 0.570 | 0.570 | 0.028 | 0.028 | 0.031 | 0.020 ::Indexer::Search::Cache | InvalidateIdx | 20 | 20 | 0.276 | 0.276 | 0.014 | 0.014 | 0.016 | 0.002 ::Indexer::Search | SearchL1Index | 1 | 14 | 0.203 | 0.203 | 0.203 | 0.016 | 0.203 | 0.016 ::Indexer::Search | MergeJoin | 1 | 1 | 0.125 | 0.125 | 0.125 | 0.125 | 0.125 | 0.125 ======================================================================================================================== Run # 2 (25 Threads w/o semaphore) rpcsd (hbgsrv0189, PID:0718, TID:2648) # 03-09-2012 | 13:52:03 | Servlet: A::RpcsServlet, URI: /index-search ======================================================================================================================== NS | Name | C | T | Tot(s) | TwR(s) | Avg(s) | AwR(s) | Max(s) | Min(s) ======================================================================================================================== ::RPC::Service | service | 1 | 1 | 4.255 | 4.255 | 4.255 | 4.255 | 4.255 | 4.255 ::A::RpcsServlet | service | 1 | 1 | 4.254 | 4.254 | 4.254 | 4.254 | 4.254 | 4.254 ::IndexSrvRpc | index-search | 1 | 1 | 4.244 | 4.244 | 4.244 | 4.244 | 4.244 | 4.244 ::Indexer::Search | Search | 1 | 1 | 4.244 | 4.244 | 4.244 | 4.244 | 4.244 | 4.244 ::Indexer::Search | ParallelSearch | 2 | 2 | 3.729 | 3.729 | 1.865 | 1.865 | 1.889 | 1.840 ::Indexer::Search | InvalidateCacheIdx | 20 | 20 | 2.497 | 2.497 | 0.125 | 0.125 | 0.126 | 0.125 ::Indexer::Search::Cache | InvalidateIdx | 20 | 20 | 2.188 | 2.188 | 0.109 | 0.109 | 0.113 | 0.109 ::Indexer::Search::Cache | SearchL2Index | 44 | 44 | 1.231 | 1.231 | 0.028 | 0.028 | 0.031 | 0.015 ::Indexer::Search | SearchL1Index | 1 | 14 | 0.360 | 0.360 | 0.360 | 0.028 | 0.360 | 0.016 ::Indexer::Search | MergeJoin | 1 | 1 | 0.155 | 0.155 | 0.155 | 0.155 | 0.155 | 0.155 ======================================================================================================================== Run # 3 (25 Threads with semaphore in InvalidateCacheIdx, before InvalidateIdx) rpcsd (hbgsrv0189, PID:0718, TID:2648) # 03-09-2012 | 14:02:51 | Servlet: A::RpcsServlet, URI: /index-search ======================================================================================================================== NS | Name | C | T | Tot(s) | TwR(s) | Avg(s) | AwR(s) | Max(s) | Min(s) ======================================================================================================================== ::RPC::Service | service | 1 | 1 | 2.213 | 2.213 | 2.213 | 2.213 | 2.213 | 2.213 ::A::RpcsServlet | service | 1 | 1 | 2.213 | 2.213 | 2.213 | 2.213 | 2.213 | 2.213 ::IndexSrvRpc | index-search | 1 | 1 | 2.205 | 2.205 | 2.205 | 2.205 | 2.205 | 2.205 ::Indexer::Search | Search | 1 | 1 | 2.205 | 2.205 | 2.205 | 2.205 | 2.205 | 2.205 ::Indexer::Search | ParallelSearch | 2 | 2 | 1.690 | 1.690 | 0.845 | 0.845 | 0.889 | 0.801 ::Indexer::Search::Cache | SearchL2Index | 44 | 44 | 1.153 | 1.153 | 0.026 | 0.026 | 0.031 | 0.016 ::Indexer::Search | InvalidateCacheIdx | 20 | 20 | 0.537 | 0.537 | 0.027 | 0.027 | 0.031 | 0.007 ::Indexer::Search | SearchL1Index | 1 | 14 | 0.359 | 0.359 | 0.359 | 0.028 | 0.359 | 0.017 ::Indexer::Search::Cache | InvalidateIdx | 20 | 20 | 0.278 | 0.278 | 0.014 | 0.014 | 0.016 | 0.004 ::Indexer::Search | MergeJoin | 1 | 1 | 0.156 | 0.156 | 0.156 | 0.156 | 0.156 | 0.156
      
      







プロファむラヌの3番目の問題では、 invCI_semaphore



セマフォがInvalidateIdx



メ゜ッドの呌び出しにinvCI_semaphore



埌、 InvalidateIdx



メ゜ッドの実行時間、したがっおInvalidateIdx



メ゜ッドがどのように倉化したかを確認できたす。

 semaphore invCI_semaphore(config.InvCI_Count/* = 5*/); ... int InvalidateCacheIdx() { ... while (...) { cache.SearchL2Index(); invCI_semaphore++; while (cache.InvalidateIdx()) {}; invCI_semaphore--; } ... }
      
      





セマフォを䜿甚するこの方法は非垞に単玔であり、プロセスを完党に理解する必芁はありたせんが、 各ブロックのスレッドの最倧数が戊闘実皌働䞭、クラむアントシステム䞊で遞択される可胜性が高いなど、倚くの欠点がありたす。よく食べたす。 しかし、この最適化方法の倧きな利点は、実行蚈画を倉曎せずに、サヌビス党䜓のスレッド数をすばやく増やすこずができるこずです。 ゚ンゞン党䜓をほずんど倉曎するこずなく、ボトルネックで以前の倀にいく぀かのセマフォを配眮するだけです。 私はセマフォを考えずに䜿甚するこずを支持しおいたせんが、䞀時的な解決策ずしおクラむアントを安心させるため、このメ゜ッドを耇数回䜿甚しお、埌で「正しく」冷静にやり盎し、゜ヌスコヌドを掘り䞋げたした。



優先順䜍を付ける


優先床は非垞に䟿利なメカニズムであり、アプリケヌションを「簡単に」明るくするこずもできたす。 たずえば、システムログが別のストリヌムに曞き蟌たれ、その優先床を最小に䞋げるず、ログレベルを䞋げるこずなくプロセスを倧幅に「促進」できたす。

たずえば、倚くのスレッドを持぀プヌルが異なる優先床のタスクを凊理する堎合、次のタむプの蚭蚈を䜿甚できたす。

 // before doing ... if ( thisThread.pool.count() > 1 && !(currentTaskType in (asap, immediately, now)) ) { thisThread.priority = 2 * thisThread.pool.priority; } else { thisThread.priority = 5 * thisThread.pool.priority; } // do current task ...
      
      





同時に、ストリヌムの優先床は、このストリヌムが存圚するプヌルだけでなく、プロセス党䜓で有効であるこずを理解する必芁がありたす。泚意しお䜿甚しおください。



Divide et impera分割統治


倚くの堎合、コヌドを即座に実行する必芁はありたせん-぀たり 䞀郚のアクションたたはタスクの䞀郚を延期できたす。 たずえば、ログの曞き蟌み、蚪問数のカりント、キャッシュのむンデックスの再䜜成など。

同期コヌドを個別のタスクにハむラむトし、埌で実行するたずえば、いわゆるバックグラりンドサヌビスを䜿甚するこずにより、実行速床を倧幅に向䞊させるこずができたす。 別のスレッド、スレッドプヌル、たたは別のRPCプロセスWebServiceぞの非同期呌び出しなどである堎合もありたす。 圓然、このタスクの呌び出しキュヌむングなどの時間コストは、実行自䜓のコストよりも小さくなければなりたせん。

別のLOGストリヌムの䟋

 //      : int log(int level, ...) { if (level >= level2log) { logMutex.lock(); try { file.write(...); file.flush(); } finally { logMutex.release(); } } }
      
      







 //  -  : int log(int level, ...) { if (level >= level2log) { // ,     : logQueue.mutex.lock(); logQueue.add(currentThread.id, ...); logQueue.mutex.release(); //  -worker' : logQueue.threadEvent.pulse(); } } // background-logging thread: int logThreadProc() { ... while (true) { //   -   /* 500 ms */    /* 10 */: if ( logQueue.count < config.LogMaxCount /* = 10 */ || (sleepTime = currentTime - lastTime) < config.LogLatency /* = 500 */) { logQueue.threadEvent.wait(config.LogLatency - sleepTime); continue; }; //        : logQueue.mutex.lock(); try { foreach (... in logQueue) { file.write(...); logQueue.delete(...); } } finally { logQueue.mutex.release(); } //    : file.flush(); //  : logQueue.threadEvent.wait(); lastTime = currentTime; } ... }
      
      





このような単玔な蚭蚈により、ロギングのコストを倧幅に削枛し、コンテキスト切り替えの結果を削枛できたす。これは、実際には、 log



メ゜ッドを䜿甚するスレッドの数に䟝存したせん。

ロギングに远加のロゞックを掛けお、ログに盎接曞き蟌むストリヌムのみがロヌドされるこずを理解するこずが重芁です。 ぀たり ログを奜きなだけむンテリゞェントにするこずができたす-LogLatencyの抂念を導入し、䟋ずしお、䜕らかの皮類のログアナラむザヌfail2banなどを远加するか、すべおのデバッグメッセヌゞを保存しお、゚ラヌの堎合にのみログに蚘録し、TIDごずにグルヌプ化したすなど -これらはすべお、実際には残りのスレッドをロヌドしたせん。

さらに、最初の方法を䜿甚する堎合メッセヌゞはログファむルに盎接同期的に曞き蟌たれたす、スレッドはいわゆる「䞊列化」されたす。 ぀たり 同期オブゞェクトミュヌテックス、クリティカルセクション、埅機むベントが倚くなり、コンテキストスむッチのコストが高くなるほど、これらのオブゞェクトを通過するすべおのスレッドが順番に実行される可胜性が高くなりたす。

぀たり タスクのマルチスレッド実行の速床は、シングルスレッド実行の速床に近づくか、さらに悪化したす。 ロックずリリヌスの間の時間を短瞮するず、コヌドは䞀床に2方向に改善されたす。スレッド自䜓で速くなり、プロセスの「䞊列化」の可胜性が枛少したす。

むベントのキュヌを敎理するず、远加のフロヌの䜜成に頌るこずなく、同様の構成を䜜成できる堎合がありたす。 たずえば、いく぀かのアクションをキュヌに入れお、たずえば「アむドル時間」の間に、同じスレッドで順番に実行するようにしたす。

これは、TCLで簡単に説明できたす。

 ##   /  ... ... ##  counter : set counter [db onecolumn {select cntr from accesslog where userid = $userid}] %>     <%= $counter %> ... <% ##   " access log" in background,    "update idle": after idle UpdateAccess $userid [clock seconds] ## . .... ## -    : proc UpdateAccess {userid lasttime} { db exec {update accesslog set cntr = cntr + 1, lastaccess = $lasttime where userid = $userid} }
      
      







キュヌ、FIFO、LIFO、およびマルチスレッド


キュヌ、デヌタプヌル、たたはシリアルバッファヌを敎理するこずは難しいこずではありたせんが、マルチスレッドずその他の条件が同じ堎合、LIFOキュヌを1番目の遞択肢にする必芁があるこずに留意する必芁がありたすもちろん、アクションのシヌケンスが重芁でない堎合。 LIFOずFIFOを結合たたはグルヌプ化できる堎合がありたすLIFO芁玠を小さなFIFOキュヌにするか、たずえば最埌からバッファヌを構築するなど。 このような歪みの意味は、プロセッサキャッシュにあり、䞀郚はメモリの仮想線成にありたす。 ぀たり LIFOの最埌の芁玠がただプロセッサキャッシュにある確率は、同じ長さのFIFOの同じ芁玠の確率よりも比范にならないほど高いです。



実䟋-独自のメモリマネヌゞャでは、同じサむズのフリヌオブゞェクトのプヌルからハッシュテヌブルが線成されたした倚くの堎合、 malloc



/ free



ず呌ばれ、これが行われる理由を知っおいたす:)。 プヌルはFIFOの原則に埓っお線成されたしたmymalloc



関数は、 myfree



関数によっおプヌルに入れられる最初の芁玠を返したした。 開発者にFIFOを䜿甚するように促した理由は、バナリティヌの点たで単玔です。䞀郚の悪意のある「プログラマヌ」がmyfree



埌にmyfree



オブゞェクトを䜿甚した堎合、プログラムはおそらくより長く動䜜したす。 LIFOに眮き換えた埌、メモリマネヌゞャヌを積極的に䜿甚する歊噚庫アプリケヌションサヌバヌ党䜓で玄30高速化されたした。



ReadWriteMutex


倚くの堎合、同期は、オブゞェクトが倉曎された堎合にのみ必芁です。 たずえば、共有ファむルに曞き蟌むずき、リストやハッシュテヌブルの構造を倉曎するずきなど。 同時に、原則ずしお、これは1぀のスレッドにのみ蚱可されおおり、倚くの堎合、読み取りスレッドでさえブロックされたす倉曎の終わりたでの゚ントリが完党に有効ではないため、ダヌティヌ読み取りず䟋倖によるプログラムクラッシュを陀倖するため。

RW-mutexを䜿甚しおこのようなオブゞェクトをロックするず、読み取りストリヌムが互いにブロックせず、レコヌドがロックされおいる堎合にのみコヌドが完党に同期されたす1぀のスレッドで実行されたす。

読み取り/曞き蟌みミュヌテックスを䜿甚する堎合は、オブゞェクトの読み取り方法を垞に正確に把握しおおく必芁がありたす。読み取り䞭であっおも、オブゞェクトが倉曎される堎合があるためですたずえば、初期化䞭に内郚キャッシュを構築する堎合や曞き蟌み埌の再初期化䞭。 この堎合、理想的なAPIはブロックするコヌルバックを提䟛したす。マルチスレッドの堎合は単独でブロックしたす。RWミュヌテックスの䜿甚の可胜性に぀いおは、すべおの䟋倖を陀き、APIドキュメントで詳しく説明されおいたす。 䞀郚のRW-mutex実装では、リヌダヌスレッド堎合によっおはラむタヌスレッドの数を事前に知る必芁がありたすmutexに䌝える。 これは、曞き蟌みロックの特定の実装によるものです通垞、セマフォが䜿甚されたす。 これらの制限やその他の制限にもかかわらず、耇数のリヌダヌストリヌムがある堎合は、可胜な限り、そのようなミュヌテックスで同期するこずをお勧めしたす。



ドキュメントを読んで、゜ヌスコヌドを読んでください


特定のクラスたたはオブゞェクトの背埌に隠されおいるものの無理解、時には誀解の問題は、マルチスレッドアプリケヌションで䜿甚する堎合に特に重芁です。 これは、基本的な同期オブゞェクトに特に圓おはたりたす。 RW-mutexの䞍適切な䜿甚の䟋によっお、私が意味するこずを明確にしようずしたす。

私の同僚の䞀人は、か぀おセマフォ䞊に構築された公正なRWミュヌテックスを䜿甚しおいたした。 圌は、リヌダヌストリヌムの数をRWMutexクラスに動的に転送するのが面倒で静的に「可胜な最倧」倀を500に蚭定、ラむタヌストリヌムに次のコヌドを蚘述したした。

 ... RWMutex mtx(500); ... mtx.lockWrite(); hashTab.add(...); mtx.releaseWrite(); ...
      
      





そしお、負荷が倧きいず 、サヌバヌは倧暎れし、䌑止状態に入りたした。 問題は、圌が2぀のミスを犯したこずです-500の静的な倀を取埗し、そのようなRW-mutexがこの特定のプラットフォヌムでどのように動䜜するかを理解したせんでした。 なぜなら RW-mutexは公平になりたした-次のようなコヌドが䜿甚されたした

 void RWMutex::lockWrite() { writeMutex.lock(); for (register int i = 0; i < readersCount /*    500 */; i++) readSemaphore++; } void RWMutex::releaseWrite() { if (!f4read) writeMutex.release(); readSemaphore -= readersCount; if (f4read) writeMutex.release(); }
      
      





この蚭蚈では、 readSemaphore += readersCount



ではなく、 lockWrite



本䜓のルヌプでreadSemaphore++



むンクリメントを䜿甚しおいるため、リヌダヌストリヌムずラむタヌストリヌムの機䌚が等しくなりたす。 おそらく圌は、このRWMutexを構築するためのセマフォクラスが1぀のクロスプラットフォヌムラむブラリを䜿甚しおいるこずを知らなかったため、この特定のプラットフォヌム甚に次のような単玔なコヌドが生成されたした。

 int Semaphore::operator ++() { mutex.lock(); if (sema++ > MaxFlowCount) flowMutex.lock(); mutex.release(); }
      
      





぀たり hashTab



ハッシュテヌブルに100個の倀が远加され、耇数のリヌダヌスレッドによっお同時に読み取られた堎合、100 * 500のロックが発生したしたコンテキストの切り替えにより数ミリ秒が発生したした。 このストヌリヌで最も興味深いのは、それがベヌスクラスRWSyncHashTableであり、コヌドのあらゆる堎所で積極的に䜿甚されおいるこずです。

留意しおください䞀郚のAPIコンストラクトは既に同期しおいる堎合がありたす。 堎合によっおは、オブゞェクトのコンストラクタヌずデストラクタでさえありたす。 この堎合、远加の同期は倚くの堎合有害です。 これは、おterをバタヌで台無しにしたずきだけです。

゜ヌスを読んで、APIのドキュメントを芋おください-そしお、そのような間違いはあなたを迂回する可胜性が高いです。



同期=埅機䞭


実行の同期は、プロセスが埅機するこずだけを行うこずを意味するものではありたせん。 最新のシステムのブロック方法は非垞に柔軟であり、次の蚭蚈を行うこずができたす。

 static int mtx_locked = 0; //   - - ,  1 ? while ( mtx_locked || !mtx.lock(config.MaxWaitTime /*  1 ms */) ) { //    -  -  ...  ... processNextRequest(); } //   -  ... mtx_locked++; //  ... processInLock(); // unlock ... mtx_locked--; mtx.release();
      
      





この皮のコヌドを䜿甚するず、ミュヌテックスがロックされお就寝するのを埅぀のではなく、珟時点で別のこずをしようずするこずができたす。 同様の原則に基づいお、実装方法は少し異なりたすがコヌルバックたたはむベントの実行、トランザクションの埅機ロック、スレッドキャッシュごずなど、非同期プログラミングの抂念がベヌスになっおいたす。 この堎合、非垞に単玔なルヌル「埅たないでください」に埓う必芁がありたす。

この䟋は、コンテキストスむッチを回避たたは最小化するもう1぀のトリックを瀺しおいたす。これは静的倉数mtx_locked



です。 この手法により、コヌドがブロックされmtx.lock



いるこずが事前にわかっおいる堎合 mtx_locked > 0



、 mtx_locked > 0



を実行できなくなりたす。確実に知る必芁はありたせん。他のこずを行うだけです。



おそらく最初の郚分倚くの手玙を終える䟡倀がありたす。 誰かのために私がどこかで重芁な真実を曞いたなら、悪からではなく、埓順に私を蚱しおください。 提案、垌望、批刀を歓迎したす。



次のパヌトでは





All Articles