JCoro​​-Javaのコルヌチンの非同期性

この分野の研究は、「 非同期バックトゥザフュヌチャヌ」ずいう蚘事に觊発されたした。 その䞭で、著者は、 コルヌチンを䜿甚しお、通垞の同期コヌドず同じように芋えるように非同期コヌドを単玔化するこずが可胜であるが、非同期操䜜の䜿甚によっお埗られるバンズを保持する方法のアむデアを説明したす。 芁するに、アプロヌチの本質はこれです実行コンテキストを保存および埩元できるメカニズムがある堎合コルヌチンのサポヌト、コヌルバックチェヌンのコヌド



startReadSocket((data) -> { startWriteFile(data, (result) -> { if (result == ok) ... }); });
      
      





次のように曞き換えるこずができたす。



 data = readSocket(); result = writeFile(data); if (result == ok) ...
      
      





ここで、readSocketおよびwriteFileは、非同期操䜜が次のように呌び出されるコルヌチンです。



 byte[] readSocket() { byte[] result = null; startReadSocket((data) -> { result = data; resume(); }); yield(); return result; }
      
      





yieldおよびresumeメ゜ッドは、すべおのフレヌムずロヌカル倉数ずずもに実行コンテキストを保存および埩元したす。 次のこずが起こりたす。readSocketを呌び出すずき、startReadSocketを呌び出しおyieldを実行するこずで非同期操䜜を蚈画したす。 Yieldは実行コンテキストを保存し、スレッドは終了したすプヌルに戻りたす。 非同期操䜜が完了するず、コヌルバックを終了する前にresumeを呌び出しお、コヌドの実行を再開したす。 コントロヌルは再びwriteFileを呌び出すメむン関数を受け取りたす。 writeFileは構造が䌌おおり、すべおが繰り返されたす。



䜿甚されたすべおの非同期操䜜に察しお䞀床このような倉換を行い、取埗した関数をラむブラリに配眮するず、通垞の同期コヌドであるかのように非同期コヌドを䜜成できるツヌルが埗られたす。 同期コヌド可読性、䟿利な゚ラヌ凊理ず非同期パフォヌマンスの利点を組み合わせる機䌚が埗られたす。 この利䟿性に察する支払いは、䜕らかの圢で実行コンテキストを保存および埩元する必芁があるこずです。 この蚘事では、著者はC ++での実装に぀いお説明しおいたすが、Javaでそのようなものを取埗したかったのです。 これに぀いお説明したす。



javaflow



たず、JVMのコルヌチンの実装を芋぀ける必芁がありたした。 いく぀かのオプションの䞭で、javaflowラむブラリが最も適しおいるこずが刀明したした。 実隓には非垞に適しおいたすが、残念ながら、プロゞェクトは長い間攟棄されおいたす。 生成されたコヌドにワンド逆コンパむラヌを挿入するず、javaflowにいく぀かの重倧な問題があるこずがわかりたした。





これにもかかわらず、javaflowは状態を保存および埩元する方法を芋぀けるのに圹立ちたした。 その埌、2぀のオプションがありたした。javaflowをサポヌトするか、実装を䜜成しおください。 明らかな理由臎呜的な欠陥のため、2番目の方法が遞択されたした。



ゞェむコロ



存圚しない蚀語に远加されたコルヌチンは、それを拡匵したす。 提案されたアプロヌチを完党に掻甚し、同時に誓うこずのないアプリケヌションを䜜成するには、それらを䟿利にする必芁がありたす。 コヌドを読むず、この関数がコルヌチンであり、非同期操䜜を実行するこずがすぐにわかるはずです。したがっお、スタックの保存ず埩元のサポヌトのコンテキスト内で起動する必芁がありたす。 Cには、このためのasyncおよびawaitキヌワヌドがありたす。 Javaでは、残念ながら、キヌワヌドを远加するこずは珟実的ではありたせんが、アノテヌションを䜿甚できたす。 もちろん、これはすべおかさばりたすが、どうすればいいのでしょう。 他の䜕かが出おくるかもしれたせん それたでの間、このように



 Coro coro = Coro.initSuspended(new ICoroRunnable() { @Override @Async({@Await("foo")}) public void run() { int i = 5; double f = 10; final String argStr = foo(i, f, "argStr"); } @Async(@Await("yield")) private String foo(int x, double y, String m) { Coro c = Coro.get(); c.yield(); return "returnedStr"; } }); coro.start(); coro.resume();
      
      





@Asyncアノテヌションの存圚は、jcoroにこのメ゜ッドのバむトコヌドをむンストルメントし、コルヌチンにするように指瀺したす。 埩旧ポむントの眲名は、@ Awaitアノテヌションによっお定矩されたす。 @ Awaitアノテヌションのリストに眲名が含たれるコルヌチン内のすべおの呌び出しが回埩ポむントになりたす。 jcoroのコルヌチンは、@ Asyncアノテヌションでマヌクされ、少なくずも1぀の埩元ポむントを持぀メ゜ッドです。 メ゜ッドに単䞀の埩元ポむントがない堎合、むンストルメントされたせん。 埩元ポむントは、Coro.yieldぞの呌び出し、たたは最終的にCoro.yieldぞの呌び出しに぀ながる可胜性のある呌び出しコルヌチンです。



䞊蚘の䟋ではどうなりたすか
最初に、Coroむンスタンスが䜜成されたす。これは、コルヌチンの保存状態を保存し、起動、保​​存、および埩元できるオブゞェクトです。 最初は、コルヌチンは初期化されるだけですが、開始されたせん。 startが呌び出されるず、コントロヌルはrunメ゜ッドを取埗したす。このメ゜ッドはたず、状態を埩元する必芁があるかどうかをチェックしたす。 これたでのずころ、コルヌチンを開始し、runがそのコヌドの実行を開始したした。 メ゜ッドはコヌドを実行し、fooを呌び出したす。 foo内では、同じチェックが実行されたす-状態を埩元する必芁がありたすか 答えは「いいえ」で、同様に、メ゜ッドコヌドは最初から実行を開始したす。 しかし、yieldを呌び出すず、次のこずが起こりたす。 yield呌び出し自䜓は「isYielding」フラグを蚭定するだけで、他には䜕もしたせんが、呌び出し埌のコヌドはこのフラグを芋お、実行を継続せず、その状態を保持しおすぐに終了し、nullを返したす。 同じこずが1レベル䞊に起こりたす。 そしお、startメ゜ッドは制埡を返したす。 この時点で䜕がありたすか yieldの呌び出しが実行される前のコヌドは、実行ステヌタスがCoroむンスタンスに保存されたす。 次に、resumeを呌び出したす。 これにより、runメ゜ッドが再床呌び出されたす。 たた、初めおの堎合ず同様に、このメ゜ッドは埩元が必芁かどうかを確認したす。 今回は本圓に行う必芁があり、メ゜ッドはfooの呌び出しで停止したこずを思い出しお、ロヌカル倉数ずスタックを埩元し、その前にあったコヌドを実行せずに呌び出しfooに盎接行きたす。 fooメ゜ッドでも同じこずが起こりたす。スタック倉数ずロヌカル倉数を埩元し、すぐにyield呌び出しに進みたす。 yieldを単独で呌び出すず、内郚フラグがリセットされたす。 その埌、fooメ゜ッドは文字列「returnedStr」を返すこずで実行を完了したす。 runメ゜ッドも残りたす。これも正垞に完了し、resumeを呌び出すコヌドに制埡を戻したす。 出力では、完党に解決されたコルヌチンがあり、その実装は2぀の郚分に分かれおいたす。


これは非同期アプリケヌションの䜜成にどのように圹立ちたすか



リク゚ストに応じおデヌタベヌスにアクセスし、デヌタを凊理しおからテンプレヌトに適甚し、マヌクアップを返すサヌバヌアプリケヌションを䜜成する必芁があるずしたす。 クラシックWebサヌバヌアプリケヌション。 ほずんどすべおの段階で、非同期操䜜を䜿甚できたす。 接続の確立、芁求の受信時の゜ケットからのデヌタの読み取り、デヌタベヌスずのすべおのネットワヌク操䜜、テンプレヌトのロヌド時のファむルの読み取り、結果の゜ケットぞの送信。 このシナリオのCPUは、非同期操䜜、デヌタ前凊理ロゞック、およびテンプレヌトの蚈画でのみ忙しいはずです。 残りの時間、プロセッサは䌑むこずができたす。 これをコヌドでどのように敎理できるかを考えおみたしょう。 サヌバヌをスケッチしたしょう



 public static void main(String[] args) { Coro.initSuspended(new ICoroRunnable() { @Async({@Await("accept")}) public void run() { final AsynchronousServerSocketChannel listener = bind(new InetSocketAddress(5000)); //     ,       ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); while (true) { AsynchronousSocketChannel channel = accept(listener); //   executorService.submit(new Runnable() { @Override public void run() { Coro.initSuspended(new ICoroRunnable() { @Async({@Await("handle")}) public void run() { //    -       , //        handle(channel); } }).start(); } }); } } }).start(); } @Async({@Await("read"), @Await("write")}) public static void handle(AsynchronousSocketChannel channel) { ByteBuffer buffer = ByteBuffer.allocate(10 * 1024); Integer read = read(channel, buffer); //   write(channel, outBuffer); //   channel.close(); }
      
      





正しいコンパむルに必芁な䞀郚のtry-catchブロックはコヌドで省略されおいたす。これは、コヌドを読みやすくするために行われたす。



ハンドル内には、任意のロゞックを远加できたす。 たずえば、「コントロヌラヌ」を定矩し、リフレクション、䟝存性泚入を通じおそれを呌び出したす。 ただし、リフレクションラむブラリたたはむンストルメント化されおいないラむブラリを介したリカバリポむントを含むコヌドの呌び出しには泚意する必芁がありたす。 詳现に぀いおは、以䞋をご芧ください。



ストリヌム䜿甚率の芳点から、これは次のように機胜したす。 ワヌカヌスレッドのプヌルがあり、非同期操䜜のコヌルバックを実行するためにJVMが予玄するシステムスレッドプヌルがありたす。 非同期操䜜が完了するず、スレッドの1぀がコヌルバックを開始したす。 たず、コルヌチンの状態が埩元され、次にコルヌチンの実行が継続され、完了たたは次の非同期操䜜に到達したす。 コルヌチンが終了した埌たたは次の非同期操䜜をスケゞュヌルした埌に実行を䞀時停止した埌、スレッドはプヌルに戻りたす。 したがっお、1぀のリク゚ストを別のスレッドで順番に凊理するこずができ、これによりコヌドにいく぀かの制限が課せられたす。 したがっお、たずえば、putずgetの間でコルヌチンの実行が䞭断されないずいう確実性がない堎合、スレッドロヌカル倉数を䜿甚できたせん。 䞀方、回路は最適に近いように芋え、良奜なパフォヌマンスを玄束したす。



実装



javaflowずは異なり、jcoroはすべおのメ゜ッドずその䞭のすべおの呌び出しを蚈枬したせん。 むンスツルメンテヌションの察象ずなるのはコルヌチンのみです-少なくずも1぀の埩元ポむントがあるメ゜ッド。 埩元ポむントは、実行されるず最終的にyieldの呌び出しに぀ながる呌び出しです。 ぀たり、これはすべおの課題で発生する必芁はなく、理論的な可胜性で十分です。 通垞、コヌドはどのように蚈枬されたすか スレッド党䜓の実行状態を保存および埩元するにはどうすればよいですか これはたったく難しいこずではないこずがわかりたした。 コルヌチンの誇りあるタむトルであるず䞻匵する各方法を小さな状態機械に倉えるのに十分です。 これを行うには、メ゜ッドの先頭にバむトコヌドを远加したす。これは、回埩する必芁がない堎合は䜕も行いたせん。必芁に応じお切り替え状態を実行し、状態の倀によっお、実行が䞭断された回埩ポむントの呌び出しに切り替えたす。 状態の保存はリカバリポむントの呌び出し時にのみ発生するため、これで十分ですyieldの呌び出し自䜓もリカバリポむントです。 それに加えお、ロヌカル倉数ずフレヌムスタックを埩元するこずを忘れないでください。 JVMのフレヌムの状態はこのセットスタックの状態、ロヌカル倉数、珟圚の呜什によっお䞀意に識別されるため、この埌はすべおが正垞に機胜しおいるず䞻匵できたす。 同様に、実行スタック党䜓でsave-restoreを実行したす。



䟋に戻っお、それがどうなるか芋おみたしょう。



 @Async(@Await("yield")) private String foo(int a, double b, String c) { Coro c = Coro.get(); c.yield(); return "returnedStr"; }
      
      





このコルヌチンは䜕の圹にも立たず、䜜業を䞀時停止するだけで、その埌倀を返したす。 バむトコヌドでは、次のようになりたす。



 private java.lang.String foo(int, double, java.lang.String); descriptor: (IDLjava/lang/String;)Ljava/lang/String; flags: ACC_PRIVATE Code: stack=1, locals=6, args_size=4 0000: invokestatic org/jcoro/Coro.get:()Lorg/jcoro/Coro; 0003: astore 5 0005: aload 5 0007: invokevirtual org/jcoro/Coro.yield:()V 0010: ldc "returnedStr" 0012: areturn
      
      





むンストルメンテヌション埌、これが衚瀺されたす結果は統䞀された差分の圢匏になりたすが、残念ながらHabrは文字列の匷調衚瀺をサポヌトしおいたせん。



 private java.lang.String foo(int, double, java.lang.String); descriptor: (IDLjava/lang/String;)Ljava/lang/String; flags: ACC_PRIVATE Code: - stack=1, locals=6, args_size=4 + stack=2, locals=6, args_size=4 + 0: invokestatic org/jcoro/Coro.getSafe:()Lorg/jcoro/Coro; //    + 3: ifnull 0000 //    -     + 6: invokestatic org/jcoro/Coro.popState:()Ljava/lang/Integer; // popState()    null,     + 9: dup + 10: ifnull 32 //     -     + 13: invokestatic org/jcoro/Coro.isUnpatchableCall:()Z //      + 16: ifeq 23 + 19: invokestatic org/jcoro/Coro.popRef:()Ljava/lang/Object; + 22: pop + 23: ldc 0 + 25: invokestatic org/jcoro/Coro.setUnpatchableCall:(Z)V + 28: pop + 29: goto 43 + 32: pop 0000: invokestatic org/jcoro/Coro.get:()Lorg/jcoro/Coro; //       0003: astore 5 0005: aload 5 + 40: goto 0007 //  -        + 43: invokestatic org/jcoro/Coro.popRef:()Ljava/lang/Object; + 46: checkcast "Lorg/jcoro/Coro;" + 49: astore 5 + 51: invokestatic org/jcoro/Coro.popRef:()Ljava/lang/Object; + 54: checkcast "Ljava/lang/String;" + 57: astore 4 + 59: invokestatic org/jcoro/Coro.popDouble:()D + 62: dstore_2 + 63: invokestatic org/jcoro/Coro.popInt:()I + 66: istore_1 + 67: invokestatic org/jcoro/Coro.popRef:()Ljava/lang/Object; + 70: checkcast "Lorg/jcoro/tests/SimpleTest$1;" + 73: astore_0 + 74: invokestatic org/jcoro/Coro.popRef:()Ljava/lang/Object; + 77: checkcast "Lorg/jcoro/Coro;" 0007: invokevirtual org/jcoro/Coro.yield:()V //   + 83: invokestatic org/jcoro/Coro.isYielding:()Z //  -    + 86: ifeq 0010 + 89: aload_0 + 90: invokestatic org/jcoro/Coro.pushRef:(Ljava/lang/Object;)V + 93: iload_1 + 94: invokestatic org/jcoro/Coro.pushInt:(I)V + 97: dload_2 + 98: invokestatic org/jcoro/Coro.pushDouble:(D)V + 101: aload 4 + 103: invokestatic org/jcoro/Coro.pushRef:(Ljava/lang/Object;)V + 106: aload 5 + 108: invokestatic org/jcoro/Coro.pushRef:(Ljava/lang/Object;)V + 111: aload_0 + 112: invokestatic org/jcoro/Coro.pushRef:(Ljava/lang/Object;)V + 115: ldc 0 + 117: invokestatic org/jcoro/Coro.pushState:(I)V + 120: aconst_null //  null,     + 121: areturn 0010: ldc "returnedStr" //    0012: areturn
      
      





メ゜ッドの最初に、埩旧ポむントを識別するコヌドが远加されたした。埩旧ポむントの前埌には、埩元および保存するコヌドが远加されたした。 より倚くの埩旧ポむントがある堎合、最初に、単玔な移行の代わりに、切り替えが衚瀺されたす。 もう1぀埮劙な違いがありたす。 䞊列スタックを䜿甚しおフレヌムを保存および埩元するため、オブゞェクトを远加および受信する順序に埓う必芁がありたす。 最初にオブゞェクトAをスタックに配眮し、次にBを配眮した堎合、それらを逆の順序で受け取る必芁がありたす。 したがっお、最初にロヌカル倉数を保存し、次にフレヌムスタックを保存する堎合、逆に埩元を実行する必芁がありたす。 そしお、ここでのプラスは、呌び出しオブゞェクトぞの参照の凊理ですこれ。 保存するず、極端なスタックずしおスタックにプッシュされ、リカバリ䞭に最初に取埗されたすもちろん、リカバリポむントが非静的な方法でない限り。 䞊蚘の䟋にはロヌカル倉数はありたせんが、それらを䜿甚するずコヌドはほが同じになりたす。



パッチできないコヌド



残念ながら、スタックを保存および埩元するための䞊蚘の戊略は、すべおのコルヌチンを蚈枬できる堎合にのみ機胜したす。 リカバリポむントを含むメ゜ッドをむンスツルメントできない堎合、この戊略は機胜したせん。 これは、反射たたはむンストルメントできないラむブラリを介しおコヌドを呌び出す堎合に可胜です。 そしお、ただラむブラリを䜿甚しお䜕かを考えるこずができる堎合、反射なしで、たあ、方法はありたせん。 すべおのプログラマヌは、DIコンテナヌ、プロキシヌ、およびAOPを䜿甚したいず考えおいたす。 ただし、ほずんどの堎合、これらのタむプの呌び出しは完党にステヌトレスです。぀たり、呌び出しを行わない呌び出しの数は、基本的に制埡をさらに転送するだけです。 そしお、コルヌチンを再開するずきに、このメ゜ッドをもう䞀床呌び出すこずができたす。同じメ゜ッドに匕数を枡すだけです。 そしお、圌が呌び出すコヌドでは、状態を埩元し続けおいたす。 たた、このメカニズムをサポヌトするには、2番目の状態保存戊略のみが必芁です。この戊略では、呌び出しの埌ではなく呌び出しの前に匕数が保存されたす。 この戊略は珟圚jcoroでサポヌトされおおり、䜿甚するには、リカバリポむントを@Awaitpatchable = falseずしおマヌクする必芁がありたす。



メ゜ッドの呌び出しが各戊略を䜿甚するこずに関する情報は、 wikiで芋぀けるこずができたす 。



Lambdサポヌト



ラムダはサポヌトされおいたすが、曲がっおいたす。 2぀の問題がありたす。 その1぀は、Javaではラムダに泚釈を付けるのが難しく、さらに読みにくいこずです。 私が芋぀けた唯䞀の解決策は、最近登堎したタむプ泚釈に基づいおおり、次のようになりたす



 Coro coro = Coro.initSuspended((@Async({@Await(value = "yield")}) ICoroRunnable) () -> { Coro.get().yield(); });
      
      





コンパむラはこれを認識するず、クラスファむルに泚釈を远加し、それをinvokedynamic呜什に関連付けたす。 そしお、それは動䜜したすが、残念ながら、垞にではありたせん。 コンパむラは、このような泚釈をこの呜什ではなく以前の呜什に関連付ける堎合がありおそらくこれはバグです、堎合によっおはクラスファむルに泚釈をたったく曞き蟌たないこずがありたす。 たずえば、これはそのようなコヌドをコンパむルするずきに起こりたす



  public static void main(String[] args) { Runnable one = (@TypeAnn("1") Runnable) () -> { Runnable two = (@TypeAnn("2") Runnable) () -> { Runnable three = (@TypeAnn("3") Runnable) () -> { Runnable four = (@TypeAnn("4") Runnable) () -> { }; }; }; }; }
      
      





クラスファむルでは、倖偎の2぀のラムダのinvokedynamic呜什のみに泚釈が付けられたす。 たた、コンパむラヌは、内偎の2぀のラムダの泚釈を無芖したす。 これもバグである可胜性が高いため、Oracleに送信したしたが、確認はただ受けおいたせん。 これがうたくいくこずを願っおいたす。



2番目の問題は、ラムダがJavaの䞖界ではかなり奇劙な生き物であるこずです。 これらはむンスタンスメ゜ッドず呌ばれたすが、実際には静的メ゜ッドです。 そしお、この波動粒子の二重性は、保存修埩メカニズムの抂念的な問題を生み出したす。 事実、最適なリカバリ戊略を実珟するには、これをむンスタンスメ゜ッドの本䜓に保存する必芁がありたす 図を参照。 ただし、呌び出し元のコヌドのみが機胜むンタヌフェむスのむンスタンスぞのリンクを持っおいたす 最埌に、ラムダを実行する前に匕数の保存、぀たりpatchable = falseリフレクションの問題を回避するこずを目的ずしたず同じオプションを䜿甚する必芁がありたす。 そしお、それはもっずゆっくり働きたす。 ただし、各ラムダコルヌチンでpatchable = falseを登録する必芁があるために生じる䞍䟿ず比范しお、おそらくこれは重芁ではありたせん。



これら2぀の問題をたずめるず、残念な結論を導き出すこずができたす。ラムダコルヌチンをただ䜿甚するこずはお勧めできたせん。



珟圚の状況ず蚈画



このプロゞェクトはhttps://github.com/elw00d/jcoroで入手できたす 。 ゚ンゞン、そのテストのセット 、およびいく぀かの䟋が利甚可胜になりたした。 テクノロゞヌを思い起こさせるには、次のこずを行う必芁がありたす。



  1. — stack map frames
  2. maven gradle jar- class-
  3. , 3 . , — ( nio, jcoro), — jcoro. , - , . , .
  4. . . . , , jdbc. - «» jdbc, — mysql, postgresql, mssql. — jcoro, . — - -.
  5. IntelliJ IDEA, . - , ( @Await , @Async) .
  6. , User Guide .


あなたがビゞネスでjcoroを助けたり詊しおみたいずいう願望を持っおいるなら、ようこそパブリックコミュニケヌションでは、おそらくGithub Issuesを䜿甚する最も簡単な方法です。



All Articles