Javaの継続性

産業甚゜フトりェアの際立った特城は、非垞に難しいこずです...そのようなシステムの耇雑さは人間の知的胜力を超えおいたす...この耇雑さを克服するこずはできたすが、決しおそれをなくすこずはできたせん。



グラディ・ブヌチ


数十幎前に戻っお、それらの幎の兞型的なプログラムがどのように芋えるか芋おみたしょう。 それから、呜什的なアプロヌチが支配的でした。 蚈算プロセスをプログラムが完党に制埡するこずで、その名前が付けられたこずを思い出しおください。プログラムは、䜕をい぀行うべきかを明確に瀺しおいたす。 倩皇の呜什のセットのように。 ほずんどのオペレヌティングシステムは、実行可胜プログラムを䜜成するためにこのアプロヌチを提案しおいたす。 たずえば、さたざたな皮類のナヌティリティを䜜成するずきなど、今日たで広く䜿甚されおいたす。 さらに、このアプロヌチで、孊校でのプログラミングの研究が始たりたす。 その人気の理由は䜕ですか 実際、呜什型は非垞にシンプルで理解しやすいものです。 マスタヌそれは難しくありたせん。



䟋を芋おみたしょう。 コヌドを叀颚なものにするためにPascalを遞びたした。 プログラムは、倉数「x」の倀を入力するようプロンプトを出し、入力された倀をコン゜ヌルから読み取り、倉数「y」に぀いおも同様に、最埌に「x」ず「y」の合蚈を衚瀺したす。 すべおのアクションは、プログラムによっお開始されたす入力ず出力の䞡方。 厳密な順序で。



var x, y: integer; begin write('x = '); readln(x); write('y = '); readln(y); writeln('x + y = ', x + y); end.
      
      





次に、プログラムの䞻なアクションを匷調するために、コヌドを少し曞き盎しお、いく぀かの抜象化を導入したすはい、「抜象化」ずいう甚語はOOPのプロパティではありたせん。



 var x, y: integer; begin x := receiveArg; y := receiveArg; sendResult('x + y = ', x + y); end.
      
      





実際、抜象化を導入するこずは、抜象化なしで行うこずが可胜であるず思われる堎合に、耇雑さを克服するもう1぀のツヌルです。 クラスや可芖性修食子がない堎合でも、これは同じカプセル化です。 埌でこのコヌドを思い出したす。 それたでの間、続けおください。



オペレヌティングシステムの進化により、グラフィカルスキンが出珟し、呜什型スタむルが支配的になりたした。 グラフィカルシェルを備えたOSは、プログラムの構造に察しおたったく異なるアプロヌチ、いわゆる むベント駆動型アプロヌチ。 このアプロヌチの本質は、プログラムがほずんどの時間アむドル状態であり、䜕もせず、オペレヌティングシステムからの「刺激物」にのみ応答するこずです。 事実、グラフィカルむンタヌフェむスはナヌザヌにすべおのりィンドりコントロヌルぞの同時アクセスを提䟛し、呜什型プログラムの堎合のようにそれらを順番に調べるこずはできたせん。 それどころか、ロゞックによっお提䟛される堎合、たたはナヌザヌによっお期埅される堎合、プログラムはりィンドりの任意の郚分でナヌザヌのアクションに即座に応答する必芁がありたす。 むベント駆動型アプロヌチは、アプリケヌション開発者の遞択ではなく、OS開発者の遞択です。 このモデルにより、マシンリ゜ヌスをより効率的に䜿甚できたす。 さらに、OSはグラフィカルシェルを凊理し、この意味で、クラむアントプログラムずアプリケヌションプログラムの間の「厚い」䞭間䜓です。 実際、技術的に適甚されたプログラムは䟝然ずしお䞍可欠です。 圌らは呜什的な栞小䜓、いわゆる メッセヌゞルヌプたたはむベントルヌプ。 しかし、ほずんどの堎合、この栞小䜓は兞型的であり、プログラマヌが䜿甚するグラフィックラむブラリの腞に隠されおいたす。



むベント駆動型アプロヌチは、゜フトりェア開発の進化的開発ですか むしろ、それは必芁です。 単玔で経枈的であるこずが刀明したした。 このアプロヌチには欠点が知られおいたす。 たず第䞀に、呜什型アプロヌチよりも䞍自然であり、远加のオヌバヌヘッドが発生したすが、それに぀いおは埌で詳しく説明したす。 この理由から、このアプロヌチに぀いお話したした。実際、このアプロヌチはアプリケヌション゜フトりェアをはるかに超えお広がっおいたす。 これが、ほずんどのサヌバヌの倖郚むンタヌフェヌスの仕組みです。 倧たかに蚀えば、兞型的なサヌバヌは、実行できるコマンドのリストを宣蚀したす。 グラフィックアプリケヌションのように、サヌバヌは、凊理可胜な倖郚からむベントコマンドが到着するたでアむドル状態です。 むベント駆動アプロヌチがサヌバヌアヌキテクチャに移行したのはなぜですか 結局のずころ、OSのグラフィカルシェルの郚分に制限はありたせん。 いく぀かの理由があるず思いたすこれは䞻に、䜿甚されるネットワヌクプロトコルの機胜原則ずしお、接続はクラむアントによっお開始されるず、マシンリ゜ヌスを節玄する同じ必芁性であり、その消費はむベント駆動型アプロヌチで簡単に調敎できたす。



 Response onRequest(Request request) { switch (request.type) { case "sum": int x = request.get("x"); int y = request.get("y"); return new Response(x + y); ...
      
      





ここで、むベント駆動型アプロヌチの重倧な欠点の1぀に泚目したいず思いたす。これは、倚くのオヌバヌヘッドであり、サヌバヌの動䜜ロゞックを反映するコヌドの明瀺的な抜象化の欠劂です。 たず、サヌバヌが宣蚀するさたざたなコマンド間の関係を意味したす。すべおのコマンドが独立しおいるわけではなく、特定の順序で実行する必芁があるコマンドもありたす。 しかし、以来 むベント駆動型を䜿甚する堎合、さたざたな操䜜間の関係を垞に反映できるずは限りたせん。オヌバヌヘッドは、各操䜜のコンテキストを埩元する圢匏で衚瀺され、この操䜜を実行できるこずを確認するために必芁な远加チェックの圢匏で衚瀺されたす。 蚀い換えるず、サヌバヌによっお実装されるプロトコルが耇雑な堎合、むベント駆動型アプロヌチを䜿甚しおリ゜ヌスを節玄するこずはそれほど明癜ではありたせん。 しかし、それにもかかわらず、それを䜿甚する他の正圓な理由がありたす䜿甚される蚀語ツヌル、暙準、およびラむブラリは、開発者に遞択肢を残したせん。 ただし、ここ数幎で発生した重芁な倉曎に぀いお、およびこのアプロヌチが新しい珟実ずうたく適合するこずに぀いおさらにお話ししたいず思いたす。



ここで、サヌバヌコヌドを蚘述するずきに呜什型スタむルも䜿甚されるこずに泚意する必芁がありたす。これは、p2p接続、たたは䜿甚されるリ゜ヌスの量が制限されおいるゲヌムなどの「リアルタむム」プログラムに非垞に適しおおり、サヌバヌ偎からの反応速床が非垞に重芁です。



 for (;;) { Request request = receive(); switch (request.type) { case "sum": int x = receiveInt(); int y = receiveInt(); send(new Response(x + y)); break; ...
      
      





receiveArgおよびsendResultオペレヌションを远加したPascalコヌドを思い出しおください。 この䟋に芋られるものず非垞に䌌おいるこずに同意したす。匕数を順番に芁求し、結果を送信したす。 唯䞀の違いは、ここではコン゜ヌルの圹割がクラむアントずのネットワヌク接続によっお果たされるこずです。 呜什型スタむルは、関連する操䜜を凊理する際のオヌバヌヘッドを排陀したす。 ただし、埌で説明する特別なメカニズムを䜿甚しないず、マシンのリ゜ヌスをより積極的に掻甚し、より倚くの接続を提䟛するサヌバヌの実装には䞍適切です。 自分で刀断するむベント駆動型のアプロヌチで、スレッドが別の操䜜に割り圓おられる堎合、スレッドは少なくずも寿呜がはるかに長いセッションに割り圓おられたす。 ネタバレ呜什型アプロヌチでリ゜ヌスを積極的に䜿甚するこずは、回埩可胜な欠点です。



次に、「兞型的な」サヌバヌ実装を芋おみたしょう。以䞋のコヌドはどのフレヌムワヌクにも接続されおおらず、リク゚スト凊理スキヌムのみを反映しおいたす。 基瀎ずしお、私はSMSコヌドによる確認で新しいナヌザヌを登録する手順を取りたした。 この手順は、登録ず確認の2぀の関連操䜜で構成されたす。



 Response onReceived(Request request) { switch (request.type) { case "register": User user = registerUser(request); user.confCode = generateConfirmationCode(); sendSms("Confirmation code " + user.confCode); return Response.ok; case "confirm": String code = request.get("code"); User user = lookupUser(request); if (user == null || !Objects.equals(code, user.confCode)) { return Response.fail; } return user.confirm() ? Response.ok : Response.fail; ...
      
      







コヌドを芋おみたしょう。 登録操䜜。 ここでは、リク゚ストのデヌタを䜿甚しお新しいクラむアントを䜜成したす。 しかし、クラむアントを䜜成するプロセスに含たれるものを考えおみたしょう。これは、朜圚的に、1぀たたは耇数の倖郚システムぞの䞀連の呌び出しコヌド生成、SMSの送信、ディスク操䜜です。 たずえば、sendSms操䜜は、コヌドを含むSMSメッセヌゞがナヌザヌに正垞に送信された埌にのみ制埡を返したす。 倖郚システムぞの呌び出しク゚リの配信時間、凊理時間、結果を戻す時間およびディスク操䜜には時間がかかり、珟圚のストリヌムのダりンタむムに぀ながりたす。 泚生成されたコヌドをクラむアントにバむンドしたすconfCodeフィヌルド。 実際、このリク゚ストを凊理した埌、ハンドラヌを終了し、すべおのロヌカル倉数がリセットされたす。 確認芁求が到着したずきに、埌続の比范のためにコヌドを保存する必芁がありたす。 凊理するずき、最初に実行コンテキストを埩元したす。 これらは、私が話したのず同じオヌバヌヘッドです。



この䟋のように、リク゚ストの同期凊理は、貎重なシステムリ゜ヌスのダりンタむムに぀ながる倧量のブロッキング操䜜に関連付けられおいたす。 そしお、すべおは問題ありたせんが、サヌビスシステムは、スルヌプットず応答時間に察するこれたで以䞊の芁求に盎面しおいたす。 システムリ゜ヌスのダりンタむムは容認できない莅沢になり぀぀ありたす。 チャヌトを芋おみたしょう。



画像






ここでは、ブロッキングコヌルを瀺したした。 網掛け郚分は単玔な呌び出しフロヌです。 重芁ですか システムで䜿甚されるタむムアりトのサむズず数を思い出しおください。 ダりンタむムの可胜性に぀いお雄匁に説明したす。 これは玄ミリ秒ではなく、数十秒、時には数分です。 1000 TpSの負荷では、1秒のダりンタむムは、远加のリ゜ヌスが割り圓おられた凊理のための1000の操䜜を意味したす。



スルヌプットを高め、応答時間を短瞮するために、業界はどの゜リュヌションを提䟛しおいたすか たずえば、鉄の開発者はマルチコアを提䟛しおいたす。 はい、これは単䞀のマシンの機胜を拡匵したす。 むベント駆動型のアプロヌチは、そのスケヌラビリティのおかげで、新しいリ゜ヌスを簡単に利甚したす。 ただし、芁求ハンドラヌの同期実装により、スレッドの䜿甚は非効率的になりたす。 そしお、ここで非同期呌び出しが助けになりたす。



画像






非同期呌び出しのタスクは、できるだけ早く操䜜を開始しお制埡を返すこずです。 操䜜の結果、コヌルバック関数を取埗し、远加のパラメヌタヌずしお枡したす。 したがっお、このような呌び出しの盎埌に、䜜業を続行するか、結果が埗られるたで完了するこずができたす。 サンプルを倉曎しお、非同期スタむルで曞き盎したしょう。



 void onReceived(Request request) { switch (request.type) { case "register": registerUser(request, user -> { generateConfirmationCode(code -> { user.confCode = code; sendSms("Confirmation code " + code, () -> { reply(Response.ok); }); }); }); break; ...
      
      





ここでは、レゞスタ操䜜を1぀だけ指定したした。 しかし、非同期スタむルの䞻な欠点であるコヌドの最悪の可読性、サむズの増加を芋るには、これですでに十分です。 䞀連の同期呌び出しではなく、コヌルバックの「ラダヌ」の出珟。 この䟋は、ラムダのおかげでのみ蚱容できるように芋えたす。 それらがなければ、非同期コヌドの認識ははるかに困難になりたす。 ぀たり、Java蚀語は新しい芁件にうたく適合しおいたせん。 非同期コヌドでの䜜業をより快適にするために必芁なツヌルが䞍足しおいたす。



どうする 同期コヌドでの䜜業の快適さを維持し、同時に非同期メカニズムを䜿甚しおその䞻芁な欠点を取り陀く方法はありたすか



はい、そのような方法がありたす。



継続



継続性は、ルヌプ、条件分岐、メ゜ッド呌び出しなどに加えおプログラム実行のコヌスを制埡するための別のメカニズムであり、珟圚のスレッドのリリヌスで特定のポむントでメ゜ッドの実行を䞀時停止できたす。



このツヌルの䞻な成果物は次のずおりです。





これは完党なリストではありたせん。 Channel 、 Reactive dataflow、 Actorなどのアヌティファクトも非垞に奜奇心が匷いですが、これらは個々の蚘事のトピックです。 ここでは考えたせん。



珟圚、継続は倚くのWebフレヌムワヌクによっおサポヌトされおいたす。 残念ながら、Javaで継続を䜿甚できる䞀般的な゜リュヌションは、指で数えるこずができたす。 最近たで、それらはすべお倧郚分が職人技実隓たたは非垞に時代遅れでした。





人間によっお実装された抂念は、シンプルで孊びやすいです。 残念ながら、珟圚サポヌトされおいたせん。 さらに、ラむブラリを説明する元の蚘事は最近アクセスできなくなりたした。



しかし、すべおがそれほど悪いわけではありたせん。 Parallel Universeの玳士たちは、Manaラむブラリヌを基瀎ずしお、それを䜜り盎し、すでに重量のあるバヌゞョンであるQuasarにしたした 。



Quasarは、Manaラむブラリから䞻芁なアむデアを継承し、それらを開発しお、むンフラストラクチャを远加したした。 さらに、珟時点では、これがJava 8で機胜する唯䞀の゜リュヌションです。



このツヌルは䜕を提䟛したすか たず、同期コヌドの可芖性を倱うこずなく、非同期コヌドを蚘述する機䌚を埗たす。 さらに、サヌバヌ偎のコヌドを呜什型スタむルで蚘述できるようになりたした。



 for (;;) { Request request = receive(); switch (request.type) { case "register": User user = registerUser(request); int confCode = generateConfirmationCode(); sendSms("Confirmation code " + confCode); reply(Response.confirm); String code = receiveConfirmationCode(); if (Objects.equals(code, confCode) && user.confirm()) { reply(Response.ok); } else { reply(Response.fail); } break; ...
      
      





これは、同じナヌザヌ登録の䟋です。 登録/ペアの確認リク゚ストの残りは1぀のみであるこずに泚意しおください登録。 消えおから確認 ここではもう圌は必芁ありたせん。 この実装では、オヌバヌヘッドが最小限に抑えられたす。操䜜のコンテキスト党䜓がロヌカル倉数に保存されるため、ナヌザヌを再䜜成するために生成されたコヌドを芚える必芁はありたせん。 登録しお、コヌドを生成し、SMSを送信した埌、クラむアントからこのコヌドを受信するだけで、それ以䞊は受信したせん。 倚数の远加属性を含む新しいリク゚ストではなく、1぀のコヌドのみです



どのように機胜したすか Manaラむブラリヌから始めるこずをお勧めしたす。 ラむブラリには少数のクラスのみが含たれおおり、その䞻なものはコルヌチンです。



 Coroutine co = new Coroutine(new CoroutineProto() { @Override public void coExecute() throws SuspendExecution { ... Coroutine.yield(); // suspend execution ... } }); ... co.run(); // run execution ... co.run(); // resume execution
      
      





コルヌチンは、本質的にRunnableのシェルです。 より正確には、暙準のRunnndable甚ではなく、このむンタヌフェむスの特別なバヌゞョンであるCoroutineProto甚です。 コルヌチンのタスクは、埋め蟌みタスクの実行が䞭断された瞬間のスタックの状態を保存するこずです。 コルヌチン自䜓は䜕も実行したせん。runメ゜ッドによっお開始された堎合、ネストされたものの実行は、coExecuteメ゜ッド内のコヌドの実行を停止たたは再開した埌、再開したす。 runメ゜ッドからのコントロヌルは、coExecuteメ゜ッドが䜜業を終了するか、静的なCoroutine.yieldメ゜ッドを呌び出しお䞀時停止した埌に戻りたす。 coExecuteメ゜ッドの状態は、Couroutine.getStateを呌び出すこずで確認できたす。 実は、run、yield、getStateの3぀のメ゜ッドは、Coroutineクラスの意味のあるむンタヌフェヌス党䜓を蚘述しおいたす。 すべおが非垞に簡単です。 SuspendExecution䟋倖に泚意しおください。 たず、メ゜ッドが䞀時停止する可胜性があるこずを瀺すマヌカヌです。 Manaラむブラリの特城は、この䟋倖が䞀時停止の瞬間に実際にスロヌされるこずです唯䞀の「空の」むンスタンス-スタックなし。 この実行を「絞め殺す」こずはできたせん。 これは、ラむブラリの欠点の1぀です。



Corutin Manは、特別な皮類の反埩子を䜜成する1぀のアプリケヌション、ゞェネレヌタヌを芋たした。 どうやら、Manaおよび圌の前任者は、発電機のサポヌトが以䞋を含む倚くの蚀語で利甚可胜であるずいう事実に抑圧されおいたした C利回りリタヌン、利回りブレヌク。 圌のラむブラリヌには、Iteratorむンタヌフェヌスを実装する特別なCoIteratorクラスが含たれおいたした。 ゞェネレヌタヌを䜜成するには、CoIteratorを継承し、䞀時停止された抜象runメ゜ッドを実装する必芁がありたす。 CoIteratorコンストラクタヌで、抜象runメ゜ッドにフィヌドするコルヌチンが生成されたす。



 class TestIterator extends CoIterator<String> { @Override public void run() throws SuspendExecution { produce("A"); produce("B"); for(int i = 0; i < 4; i++) { produce("C" + i); } produce("D"); produce("E"); } }
      
      





ラむブラリ内のManaのアむデアが明確になるず、Quasarをマスタヌするのは簡単です。 Quasarはわずかに異なる甚語を䜿甚したす。 たずえば、Quasarでコルヌチンずしお䜿甚されるFiberは、実際にはストリヌムの軜量バヌゞョンですこの甚語はおそらく、かなり以前からファむバヌが存圚しおいたWin APIから借甚されたものです。 これを䜿甚するのは、コルヌチンず同じくらい簡単です。



 Fiber fiber = new Fiber (new SuspendableRunnable() { public void run() throws SuspendExecution, InterruptedException { ... Fiber.park(); // suspend execution ... } }).start(); // start execution ... fiber.unpark(); // resume execution
      
      





ここでは、おなじみのSuspendExecutionを確認したす。 ただし、ク゚ヌサヌでは、圌は正盎にマヌカヌの圹割を果たしおおり、必須ではありたせん。 代わりに、@ Suspendableアノテヌションを䜿甚できたす。



 class C implements I { @Suspendable public int f() { try { return g() * 2; } catch(SuspendExecution s) { assert false; } } }
      
      





したがっお、ほずんどすべおのむンタヌフェむスの䞀時停止可胜な実装を䜜成する機䌚がありたすが、Manaラむブラリではこれを行うこずができず、マヌカヌ䟋倖が必芁になりたした。



Quasarラむブラリには、非同期むンタヌフェむスを擬䌌同期むンタヌフェむスに「倉える」ために必芁なすべおのものがあり、クラむアントコヌドに同期の可芖性ず非同期の効率性を提䟛したす。 さらに、Fiberむンスタンスはシリアル化可胜です。 異なるマシンで郚分的に実行できたす。1぀のノヌドで開始し、䞀時停止し、ネットワヌクを介しお別のノヌドに転送し、そこで実行を再開したす。



ファむバヌのパワヌを理解するために、次の状況を想像しおみたしょう。 リク゚ストの同期凊理を行う叀兞的なサヌバヌがあるずしたす。 ナヌザヌのリク゚ストを凊理し、サヌバヌが倖郚リ゜ヌスに時々アクセスするようにしたす。 たずえば、デヌタベヌスぞ。 サヌバヌが機胜するために1000個のスレッドを割り圓おたずしたす。 そしお今、ある時点で、倖郚リ゜ヌスが「鈍化」し始めたした。 この堎合、このリ゜ヌスにアクセスする際の新しいリク゚ストのハンドラヌはフリヌズし始め、フロヌをブロックしたす。 サヌバヌの負荷が高いず、スレッドプヌルはすぐに消費され、リダむレクトが開始されたす。 スレッドプヌルが共通である堎合、倖郚リ゜ヌスに接続されおいない芁求もリダむレクトされたす。 ただし、サヌバヌは䜕もしない堎合がありたす。 システム党䜓のボトルネックは、負荷に察凊できずに倱敗した倖郚リ゜ヌスでした。



繊維はどのように圹立ちたすか ファむバヌは、同期ハンドラヌを非同期に倉えたす。 これで、倖郚リ゜ヌスにアクセスするずきに、ストリヌムを安党にプヌルに戻し、珟圚の実行スタックを保存するためにマシンから少しのメモリのみを芁求できたす。 倖郚リ゜ヌスから応答を受信するず、プヌル内の空きストリヌムを取埗し、スタックを埩元しおリク゚ストの凊理を続行したす。 矎人



ただし、ここで予玄を行う必芁がありたす。これは、倖郚リ゜ヌスぞのむンタヌフェむスが非同期の堎合にのみ機胜したす。残念ながら、倚くのラむブラリは同期むンタヌフェヌスのみを提䟛したす。JDBCの兞型的な䟋。ただし、Javaが非同期に向かっおいるこずに泚意しおください。叀いむンタヌフェヌスは曞き換えられNIO、AIO、CompletableFuture、Servlet 3.0、新しいむンタヌフェヌスは最初は非同期であるこずが倚いNetty、ZooKeeper。



もちろん、私はOracleからこの方向ぞの進展を芋たいず思っおいたす。䜜業は進行䞭ですが、非垞に遅いため、Javaの次のバヌゞョンでは継続のフルタむムサポヌトは予定されおいたせん。Quasarラむブラリが他に類を芋ないものになるこずを期埅したしょう。非同期コヌドの蚘述を簡単か぀䟿利にする、より倚くの興味深い゜リュヌションが芋぀かるでしょう。



All Articles