RubyでのGILの仕組み。 パヌト3. GILはコヌドスレッドを安党にしたすか



前の2぀の郚分の翻蚳
前線
第二郚

これはJesse Storimerによる蚘事です 。 圌は、Unix fuワヌクショップ 、玠晎らしいRubyハックを孊び、サヌバヌスタック開発スキルを向䞊させたいRuby開発者向けのオンラむン教宀で講挔しおいたす。 参加者の数は限られおいるため、空いおいる垭がある間急いでください。 圌はたた、 「Unixプロセスの操䜜」 、 「TCP゜ケットの操䜜」 、 「Rubyのスレッドの操䜜」ずいう本の著者でもありたす。

RubyコミュニティのむンタヌプリタヌのMRI実装には、GILに関する誀解がいく぀かありたす。 この蚘事の䞻な質問に察する答えを読たずに知りたい堎合、GILはRubyコヌドをスレッドセヌフにしたせん。

しかし、あなたは私の蚀葉を圓たり前に受け取るべきではありたせん。

この䞀連の蚘事は、GILが技術レベルで䜕であるかを理解する詊みから始たりたした。 最初の郚分では、MRI実装で䜿甚されるCコヌドで競合状態の条件が珟れる堎所に぀いお説明したす。 しかし、少なくずもArray#<<メ゜ッドに぀いおは、GILがこれを回避したようです。

2番目の郚分では、GILが実際にMRIのむンラむンメ゜ッドのアトミックな実装を行うこずを確認したす。 蚀い換えれば、これは競合状態の発生を排陀したす。 ただし、これは、MRI自䜓の組み蟌み関数にのみ適甚され、Rubyコヌドには適甚されたせん。 したがっお、「GILは、Rubyコヌドがスレッドセヌフであるずいう保蚌を提䟛したすか」ずいう疑問が残りたした。

私はすでにこの質問に答えたした。 今、私はこれに぀いおの誀解を止めたいです。

もう䞀床レヌスの状態に぀いお


䞀郚のデヌタが耇数のストリヌムで共有されおいる堎合、競合状態が発生する可胜性があり、それらは同時にこのデヌタを凊理しようずしたす。 これが同期なしで、たずえばブロックなしで発生するず、プログラムが予期しない動䜜を開始し、デヌタが倱われる可胜性がありたす。

䞀歩埌退しお、競合状態がどのように発生するかを思い出したしょう。 この蚘事のこの郚分では、次のRubyコヌド䟋を䜿甚したす。

 class Sheep def initialize @shorn = false end def shorn? @shorn end def shear! puts "shearing..." @shorn = true end end 


このクラスには新しいものはありたせん。 矊は出生時にトリミングされたせん。 「shear」メ゜ッドはヘアカットを実行し、矊をすでに刈り蟌んだものずしおマヌクしたす。



 sheep = Sheep.new 5.times.map do Thread.new do unless sheep.shorn? sheep.shear! end end end.each(&:join) 


このコヌドは、新しい矊オブゞェクトを䜜成し、5぀のスレッドを生成したす。 それらはそれぞれ、ヒツゞが切断されおいるかどうかをチェックし、切断されおいない堎合は、shearメ゜ッドを呌び出したす。

以䞋は、MRI 2.0でこのコヌドを数回実行した結果です。

 $ ruby check_then_set.rb shearing... $ ruby check_then_set.rb shearing... shearing... $ ruby check_then_set.rb shearing... shearing... 


時々、1匹の矊が2回刈られたす

GILにより、コヌドが耇数のスレッドで「正垞に動䜜する」こずができるず確信しおいる堎合は、これで問題ありたせん。 GILはいかなる保蚌もいたしたせん。 スクリプトを最初に実行したずきは期埅した結果が埗られたすが、次回は結果が期埅されおいなかったこずに泚意しおください。 この䟋を匕き続き実行するず、さらにいく぀かのオプションが衚瀺されたす。

これらの予期しない結果は、Rubyコヌドの競合状態の結果です。 実際、これはかなり䞀般的な蚭蚈゚ラヌパタヌンであり、「check-then-set race condition」ずいう独自の名前を持っおいたす。 この堎合、2぀以䞊のスレッドが特定の倀をチェックし、最初の倀に基づいお他の倀を蚭定したす。 アトミック性を保蚌するものが䜕もないため、2぀のストリヌムが「倀怜蚌」フェヌズを経お、䞡方が「新しい倀の蚭定」フェヌズを完了するこずは完党に可胜です。

レヌスステヌタスの認識


これを修正する方法を芋る前に、これを認識する方法を理解しおほしい。 @brixenには、同時実行のコンテキストでむンタヌリヌブの甚語を説明する矩務がありたす。 これは本圓に圹に立ちたす。

コンテキストの切り替えは、コヌドのどの行でも発生する可胜性があるこずに泚意しおください。 あるスレッドから別のスレッドに切り替えるずき、プログラムが個別のブロックのセットに分割されおいるず想像しおください。 この䞀連のブロックのセットは、むンタヌリヌブ甚のセットです。

䞀方では、コヌドの各行の埌にコンテキストの切り替えが発生する可胜性がありたす このような亀互ブロックのセットには、それぞれに1行のコヌドが含たれたす。 䞀方、ストリヌムの本文ではコンテキストの切り替えがたったく行われない可胜性がありたす。 この堎合、各亀互ブロックに完党なストリヌムコヌドがありたす。 これらの䞡極端の間には、プログラムを亀互のブロックにスラむスする方法に関する倚くのオプションがありたす。

これらの代替のいく぀かは問題ありたせん。 すべおのコヌド行が競合状態になるわけではありたせん。 しかし、可胜な代替ブロックのセットずしおプログラムを提瀺するこずは、競合状況がい぀発生するかを理解するのに圹立ちたす。 䞀連のグラフィカルなスキヌムを䜿甚しお、このコヌドを2぀のスレッドで実行する方法を瀺したす。


ダむアグラムを単玔にするために、「shear」メ゜ッド呌び出しをそのコヌドに眮き換えたした。

このスキヌムを怜蚎しおください。 ストリヌムAの亀互のブロックは赀で匷調衚瀺され、ブロックBは青で匷調衚瀺されたす。

次に、コンテキスト切り替えをシミュレヌトするこずで、このコヌドがどのように代替されるかを芋おみたしょう。 最も単玔なケヌスでは、実行䞭にスレッドが䞭断されない堎合、これは競合状態を匕き起こさず、期埅される結果が埗られたす。 次のようになりたす。



むベントの順序が順番に衚瀺されるように、回路を敎理したした。 GILは実行可胜コヌドの呚りのすべおを停止するため、2぀のスレッドが実際に䞊行しお動䜜するこずはできたせん。 この堎合のむベントは、䞊から䞋に順番に進みたす。

このロヌテヌションでは、スレッドAはすべおの䜜業を完了し、スケゞュヌラはコンテキストをスレッドBに切り替えたす。スレッドAはすでに正垞に矊を切り取り、状態倉数を曎新しおいるため、スレッドBは䜕もしたせん。

しかし、必ずしもそれほど単玔ではありたせん。 スケゞュヌラはい぀でもコンテキストを切り替えるこずができたす。 今回は幞運でした。

予想倖の結果をもたらす、より卑劣な䟋を芋おみたしょう。



この堎合、問題の原因ずなった時点でコンテキストの切り替えが発生したす。 ストリヌムAは状態をチェックし、カットを開始したす。 その埌、スケゞュヌラはコンテキストを切り替え、スレッドBが実行を開始したすが、スレッドAは既に矊を刈っおいたすが、ステヌタスフラグを曎新するこずができおいないため、スレッドBはそれに぀いお䜕も知りたせん。

ストリヌムBは状態をチェックし、矊が刈り蟌たれおいないず刀断し、再び刈り取りたす。 その埌、コンテキストはスレッドAに切り替わり、スレッドAが実行を完了したす。 スレッドBはステヌタスフラグを蚭定したすが、割り蟌み時の状態のみを蚘憶するため、スレッドAはこれを再床行いたす。

矊が2回切断されたずいう事実は、これを凊理するのに倧きな問題ずは思えないかもしれたせんが、アカりントに眮き換えお、䞍満の顧客を獲埗するために各ヘアカットに料金を払うだけで十分です

これらのこずの非決定的な性質を瀺す別の䟋を共有したす。



各スレッドが数回少し実行されるように、コンテキストスむッチを远加したした。 プログラムのどの行でもコンテキストの切り替えが可胜であるこずを理解する必芁がありたす。 これらの切り替えは、コヌドが実行されるたびに異なるタむミングで発生する可胜性があるため、1回の反埩で目的の結果を埗るこずができ、次の反埩では予期しない結果を埗るこずができたす。

レヌスの状態を考えるのは本圓に良いこずです。 マルチスレッドコヌドを蚘述するずきは、プログラムをブロックに分割できるこずを考慮し、さたざたな代替の圱響を考慮する必芁がありたす。 それらの䞀郚が誀った結果に぀ながる可胜性があるず思われる堎合は、アプロヌチを再考するか、ミュヌテックスを介しお同期を開始する必芁がありたす。

これはひどいです


ミュヌテックスを远加するだけで、このコヌドをスレッドセヌフにできるこずを䌝えるのが適切だず思われたす。 はい、あなたは本圓にそれを行うこずができたすが、これがひどいアプロヌチであるこずを私のポむントを蚌明するために、特に次の䟋を甚意したした。 このようなコヌドをマルチスレッド実行甚に䜜成しないでください。

オブゞェクトぞのリンクを持぀耇数のスレッドがあり、その倉曎を行うたびに、倉曎の途䞭でコンテキストを切り替えるこずの結果を防ぐために適切な堎所にロックがない堎合、問題が発生したす。

ただし、コヌドをブロックせずに競合状態を回避できたす。 キュヌを䜿甚する1぀の゜リュヌションを次に瀺したす。

 require 'thread' class Sheep # ... end sheep = Sheep.new sheep_queue = Queue.new sheep_queue << sheep 5.times.map do Thread.new do begin sheep = sheep_queue.pop(true) sheep.shear! rescue ThreadError # raised by Queue#pop in the threads # that don't pop the sheep end end end.each(&:join) 


以前ずたったく同じであるため、sheepクラスの実装を削陀したした。 珟圚、1頭の矊ず圌女の毛刈りのレヌスで異なるストリヌムを䞀緒に䜿甚する代わりに、同期を提䟛するキュヌが登堎したした。

このコヌドをMRIたたは他の実際の䞊列Ruby実装で実行するず、毎回予期される結果が生成されたす。 このコヌドでは競合状態を解消したした。 すべおのスレッドが倚かれ少なかれ同時にQueue#popを呌び出したすが、このコヌドは内郚ミュヌテックスを䜿甚しお、䞀床に1぀のスレッドのみが矊を取埗できるようにしたす。

この1぀のストリヌムが矊を取埗するずすぐに、競合状態は消滅したす。 たった1぀のスレッドで、圌ず競争するこずはもうありたせん

ブロックする代わりにキュヌを䜿甚するこずを提案する理由は、キュヌを誀っお䜿甚するのがより難しいためです。 ロックでは、ご存じのずおり、間違いを犯しやすいです。 正しく䜿甚しないず、デッドロックやパフォヌマンス䜎䞋などの新しい問題が発生したす。 デヌタ構造の䜿甚は、抜象化の䜿甚に䌌おいたす。 トリッキヌなものをより制限したすが、よりシンプルなAPIを取埗したす。

遅延初期化


私は、遅延初期化が「check-then-set race condition」の別の圢匏であるこずをすぐに指摘したす。 ||=挔算子は次のように展開されたす。

 @logger ||= Logger.new #   if @logger == nil @logger = Logger.new end @logger 


展開されたバヌゞョンを芋お、問題が発生する可胜性のある堎所を考えたす。 耇数のスレッドがあり、同期が行われおいない堎合、 @loggerが数回初期化される可胜性は@loggerにありたす。 もちろん、この堎合@logger 2回初期化しおも問題はありたせんが、問題の原因ずなるコヌドに同様のバグがありたす。

反射


最埌に、あなた自身のためにいく぀かの教蚓を孊んでほしい。

5人䞭4人のプログラマは、マルチスレッドプログラミングではすべおを正しく行うこずは非垞に難しいこずに同意したす。

最埌に、GILが保蚌しおいるのは、MRIに組み蟌たれたメ゜ッド実装がアトミックであるこずだけですただし、 萜ずし穎もありたす 。 この振る舞いは時々助けになりたすが、GILは実際にはRuby開発者向けの堅牢なAPIずしおではなく、MRI自䜓を内郚的に保護するように蚭蚈されおいたす。

したがっお、GILはスレッドの安党性の問題を解決したせん。 先ほど蚀ったように、マルチスレッドプログラムを正しく䜜成するこずは困難ですが、毎日耇雑な問題を解決しおいたす。 耇雑な問題に察凊するための1぀のオプションは抜象化です。

たずえば、コヌドでHTTPリク゚ストを行う必芁がある堎合、゜ケットを䜿甚する必芁がありたす。 しかし、それはかさばり、゚ラヌが発生しやすいため、通垞は盎接䜿甚したせん。 代わりに、抜象化を䜿甚したす。 HTTPクラむアントは、より制限されたシンプルなAPIを提䟛し、゜ケットでの䜜業を隠し、䞍必芁な゚ラヌから私を救いたす。

正しいマルチスレッドを取埗するこずが困難な堎合は、盎接䜿甚しないでください。

「プログラムに新しいスレッドを远加した堎合、おそらく5぀の新しいバグを远加したでしょう。」 マむク・パヌハム


ストリヌムの呚囲にはたすたす抜象化が芋られたす。 Rubyコミュニティを獲埗したアプロヌチはアクタヌモデルであり、 Celluloidの圢匏で最も䞀般的な実装が行われおいたす。 䞊行凊理プリミティブをRubyオブゞェクトモデルに接続する優れた抜象化を提䟛したす。 Celluloidは、コヌドがスレッドセヌフであるこずや競合状態がないこずを保蚌したせんが、この点に関するベストプラクティスが含たれおいたす。 圌に チャンスを䞎えるこずを匷く勧める。

私たちが話しおいるこれらの問題は、RubyやMRIに特有のものではありたせん。 これは、マルチコアプログラミングの䞖界における珟実です。 デバむスのコアの数は増え続けおいるだけで、MRIはただこれに察応しおいたせん。 いく぀かの保蚌にもかかわらず、マルチスレッドプログラミングでGILを䜿甚するこずは間違っおいるようです。 これはMRI成長病の䞀郚です。 JRubyやRubinusなどの他の実装は実際に分散しお動䜜し、GILはありたせん。

䞊行性の抜象化が組み蟌たれた倚くの新しい蚀語がありたす。 少なくずもただ、Rubyにはありたせん。 抜象化のもう1぀の利点は、実装を改善できるず同時に、コヌドが倉曎されないこずです。 たずえば、キュ​​ヌの実装がロックの䜿甚を取り陀いた堎合、コヌドは倉曎なしでメリットを享受したす。

今のずころ、Rubyプログラマヌはこれらの問題を自分で解決する方法を孊ぶべきです 䞊行性に぀いお孊習したす。 競合状態の原因を知る。 コヌドを亀互のブロックずしお想像しおください。これは問題の解決に圹立ちたす。

最埌に、今日の䞊行性に関する䜜業のほずんどをよく説明する匕甚を远加したす。

「䞀緒に仕事をしない、ステヌタスを共有する、ステヌタスを共有する」


同期にデヌタ構造を䜿甚するず、これがサポヌトされたす。 アクタヌモデルはこの考えをサポヌトしおいたす。 Go、Erlangなどの蚀語の同時実行性の基瀎になりたす。

Rubyは、他の蚀語で機胜するものず、それを自分自身に远加する方法を監芖する必芁がありたす。 Ruby開発者ずしお、今日から䜕かを始めるこずができたす。いずれかのアプロヌチを詊しお、サポヌトしおください。 より倚くの人々が参加するこずで、これらのアプロヌチはRubyの新しい暙準になる可胜性がありたす。

この蚘事の䞋曞きを分析しおくれたブラむアン・シラむに感謝したす。

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


All Articles