Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The rest of "Loom"

The rest of "Loom"

Monthly Java spec study session powered by JJUG (Japan Java Users Group) on 27, July, 2023.

Akihiro Nishikawa

July 27, 2023
Tweet

More Decks by Akihiro Nishikawa

Other Decks in Technology

Transcript

  1. The rest of "Loom" Structured concurrency and Scoped Values Virtual

    threads 以外の “Project Loom” NISHIKAWA, Akihiro Cloud Solution Architect, Microsoft Japan
  2. Project Loomの目的 “Project Loom is to intended to explore, incubate

    and deliver Java VM features and APIs built on top of them for the purpose of supporting easy-to-use, high-throughput lightweight concurrency and new programming models on the Java platform. “ Main - Main - OpenJDK Wiki Javaプラットフォーム上で使いやすく、高スループットの軽量な並行処理と新しいプログラミングモデルをサポートするための、 Java VMの機能とAPIを提供することを目指したプロジェクト
  3. Project Loom  Virtual Threads  JEP 444: Virtual Threads

    (openjdk.org)  Structured Concurrency (Preview)  JEP 453: Structured Concurrency (Preview) (openjdk.org)  Scoped Values (Preview)  JEP 446: Scoped Values (Preview) (openjdk.org)
  4. 今日のスコープ  Virtual Threads  JEP 444: Virtual Threads (openjdk.org)

     Structured Concurrency (Preview)  JEP 453: Structured Concurrency (Preview) (openjdk.org)  Scoped Values (Preview)  JEP 446: Scoped Values (Preview) (openjdk.org)
  5. Virtual Threadsと並列・並行処理については... Virtual Threads - 導入の背景と、効果的 な使い方 - - Speaker

    Deck Javaの並列/並行処理の基本 - Speaker Deck A 007 Virtual Threads 導入の背景と、効果的な使い方 - YouTube Java仕様勉強会「Javaの並列/並行処理」 - YouTube
  6. 経緯  JEP 428 (JDK 19)  Incubator  JEP

    437 (JDK 20)  Second Incubator  Scoped Valuesを継承するようにアップデート  JEP 453 (JDK 21)  java.util.concurrentパッケージのPreview API  StructuredTaskScope::fork(...)メソッドの戻り値をFutureからSubtaskに変更
  7. 目指していること・いないこと 目指していること  スレッドリークやキャンセル遅延など、キャン セルやシャットダウンに起因する一般的な リスクを排除できる並行プログラミングのス タイルを推進する  並列プログラムコードのObservabilityを向 上

    目指していないこと  java.util.concurrentパッケージの 並行処理コンストラクトの置き換え  Java Platform用の決定的な構造化並 行APIの定義  スレッド間でデータのストリームを共有する 手段(つまりチャネル)の定義(現段階 では)  既存のスレッド中断メカニズムから新しいス レッドキャンセル機構への置き換え(現段 階では)
  8. 並列処理 Subtask 1 Subtask 2 Subtask 3 Single thread Subtask

    1 Subtask 2 Subtask 3 Multi threads リソースがあるなら、並行実行によりタスク全体の処理時間は 小さくなるが、スレッドの管理やトラブルシューティングが大変 (Subtask2が落ちたら他のSubtaskはどうすれば...) サブタスクは順次実行するので追跡は簡 単だが、タスク全体の処理時間は大きく なる
  9. java.util.concurrent.ExecutorService API // ExecutorServiceは作成済み private final ExecutorService esvc = ...;

    Response handle() throws ExecutionException, InterruptedException { Future<String> user = esvc.submit(() -> findUser()); Future<Integer> order = esvc.submit(() -> fetchOrder()); // findUserの終了待ち String theUser = user.get(); // fetchOrderの終了待ち int theOrder = order.get(); return new Response(theUser, theOrder); } futureのget()メソッドでサブタスクの結果 を待っている(ブロッキング呼び出し)
  10. サブタスクが失敗したときの挙動 findUser()が 例外をスローした場合 • handle()はuser.get()を呼び出すときに例外をスローする • fetchOrder()はそれ自身のスレッドで実行し続ける handle()を実行している スレッドが中断された場合 •

    中断はサブタスクに伝搬しない • findUser()とfetchOrder()の両スレッドはリークする • handle()が失敗した後も実行を継続する findUser()の実行に長 い時間がかかり、その間に fetchOrder()が失敗し た場合 • handle()はfindUser()をキャンセルせずuser.get()をブロックし、 不必要にfindUser()を待つ • findUser()が完了し、user.get()が返された後にのみ、 order.get()が例外をスローし、handle()が失敗する
  11. 並列・並行処理時にも同じようにできないか?  実現できれば、並行処理がより簡単、より信頼性高く、監視もしやすくなる  構文構造によってサブタスクの寿命を明確にできる  スレッド内コールスタックに類似したスレッド間階層の実行時表現が可能  この表現を使えば、 

    エラー伝播とキャンセルが可能  並行プログラムの監視も容易になる(はず) 並行タスクに構造を課すためのAPI (java.util.concurrent.ForkJoinPool) はすでに存在するものの、I/Oを伴うタスクではなく、計算集約的なタスクのために設計されて いるもので、使い勝手が悪い
  12. 原理 “If a task splits into concurrent subtasks, then they

    all return to the same place, namely the task's code block.” (タスクが並行サブタスクに分割されたら、それらはすべて同じ場所、つまりタスクの コードブロックに戻る) そのために 1. コードのブロックを流れる実行の入口と出口を明確に定義されなければならない 2. コードの構文上の入れ子構造を反映するように、操作のライフタイムが厳密に入れ子構造になっている必要 がある
  13. StructuredTaskScope java.util.concurrentパッケージ  タスクをサブタスクのファミリーとして構造化  サブタスクはそれぞれのスレッドで実行  個々にフォーク  ユニットとしてjoin、キャンセル

     サブタスクの結果や例外は集約され、親タスクが処理  サブタスクのライフタイム  明確なレキシカル・スコープに限定  そのライフタイム中に、タスクとサブタスクのすべてのやり取りを行う (フォーク、結合、キャンセル、エラー処理、 結果の合成)
  14. java.util.concurrent.StructuredTaskScope API public class StructuredTaskScope<T> implements AutoCloseable { public <U

    extends T> Subtask<U> fork(Callable<? extends U> task); public void shutdown(); public StructuredTaskScope<T> join() throws InterruptedException; public StructuredTaskScope<T> joinUntil(Instant deadline) throws InterruptedException, TimeoutException; public void close(); protected void handleComplete(Subtask<? extends T> handle); protected final void ensureOwnerAndJoined(); }
  15. StructuredTaskScopeでhandleメソッドを書き換え Response handle() throws ExecutionException, InterruptedException { try (var scope

    = new StructuredTaskScope.ShutdownOnFailure()) { Supplier<String> user = scope.fork(() -> findUser()); Supplier<Integer> order = scope.fork(() -> fetchOrder()); // 両サブタスクを待ち受け、エラー発生時には例外を投げる scope.join().throwIfFailed(); // 両サブタスクが成功したら、両者の結果をまとめる return new Response(user.get(), order.get()); } } 元のhandle()
  16. StructuredTaskScopeでhandleメソッドを書き換え Response handle() throws ExecutionException, InterruptedException { try (var scope

    = new StructuredTaskScope.ShutdownOnFailure()) { Supplier<String> user = scope.fork(() -> findUser()); Supplier<Integer> order = scope.fork(() -> fetchOrder()); // 両サブタスクを待ち受け、エラー発生時には例外を投げる scope.join().throwIfFailed(); // 両サブタスクが成功したら、両者の結果をまとめる return new Response(user.get(), order.get()); } } ① スレッドのライフタイムはtry-with-resources内に限定 ② findUser()サブタスクまたはfetchOrder()サブタスクのいずれかが失敗した場合、それがまだ完了していな ければ、もう一方のサブタスクはキャンセル (ShutdownOnFailureのシャットダウン・ポリシーによる) ③ join()の呼び出しの前または呼び出し中にhandle()を実行しているスレッドが割り込まれた場合、そのスレッド がスコープを抜けると、両方のサブタスクが自動的にキャンセル ④ thread dumpはタスク階層を明確に表す 【例】findUser()とfetchOrder()を実行しているスレッドはスコープの子として表示 ① ②
  17. 通常はtry-with- resourcesで暗黙 的にスコープを閉じる スコープ内でjoin()または joinUntil(java.time.Instan t)の呼び出しは必須 任意のタイミングでス コープの shutdown()メソッド を呼び出せる

    fork(Callable )メソッドを使用 スコープの終了 エラー処理や 結果の集計 スコープオーナー がスコープのjoin を指示 (Optional) 未完サブタスク のキャンセル、 新サブタスクの フォーク抑止 スコープ内の サブタスクを フォーク StructuredTaskScopeを使用する場合の流れ スコープを作成した スレッドがスコープの オーナー スコープの作成
  18. shutdown-on-failureポリシーを持つStructuredTaskScope <T> List<T> runAll(List<Callable<T>> tasks) throws InterruptedException, ExecutionException { try

    (var scope = new StructuredTaskScope.ShutdownOnFailure()) { List<? extends Supplier<T>> suppliers = tasks.stream().map(scope::fork).toList(); scope.join() .throwIfFailed(); // サブタスクが失敗すると例外を伝播 // すべてのタスクが成功したら、結果をまとめる return suppliers.stream().map(Supplier::get).toList(); } }
  19. shutdown-on-successポリシーを持つStructuredTaskScope <T> T race(List<Callable<T>> tasks, Instant deadline) throws InterruptedException, ExecutionException,

    TimeoutException { try (var scope = new StructuredTaskScope.ShutdownOnSuccess<T>()) { for (var task : tasks) { scope.fork(task); } // 一定期間まで待機し、どのサブタスクも成功しなければ例外を送出 return scope.joinUntil(deadline) .result(); } }
  20. 正常に完了したサブタスクの結果を収集するStructuredTaskScopeサブクラスの例 class MyScope<T> extends StructuredTaskScope<T> { private final Queue<T> results

    = new ConcurrentLinkedQueue<>(); MyScope() { super(null, Thread.ofVirtual().factory()); } @Override protected void handleComplete(Subtask<? extends T> subtask) { if (subtask.state() == Subtask.State.SUCCESS) results.add(subtask.get()); } @Override public MyScope<T> join() throws InterruptedException { super.join(); return this; } // 成功完了したサブタスクからの結果ストリームを返す public Stream<T> results() { super.ensureOwnerAndJoined(); return results.stream(); } } // 使い方 <T> List<T> allSuccessful(List<Callable<T>> tasks) throws InterruptedException { try (var scope = new MyScope<T>()) { for (var task : tasks) scope.fork(task); return scope.join() .results().toList(); } }
  21. スコープオーナーによる結果の処理 シャットダウンポリシーがサブタス クの結果を処理しない場合 シャットダウンポリシーがサブタス クの結果を処理する場合 シャットダウンポリシーを使わず、 サブタスクの例外を処理して複 合した結果を生成する場合 • fork(...)で返されたSubtaskオブジェクトを使い、サブタスク

    の結果を処理 • スコープオーナーが呼び出すSubtaskメソッド: ほとんどの場合、 get()メソッドのみ • fork(...)メソッドはvoidを返すものとして扱う • サブタスクは、ポリシーによる集中例外処理後にスコープオーナーが 処理すべき情報を結果として返す • 例外はサブタスクからの値として返すことができる • たとえば、タスクのリストを並列に実行し、各タスクの成功または例 外の結果を含む、完了したFutureのリストを返すことも可能
  22. タスクのリストを並列に実行し、各タスクの成功または例外の結果を含む、完了したFutureのリストを返すメソッド <T> List<Future<T>> executeAll(List<Callable<T>> tasks) throws InterruptedException { try (var

    scope = new StructuredTaskScope.ShutdownOnFailure()) { List<? extends Supplier<Future<T>>> futures = tasks.stream() .map(task -> asFuture(task)) .map(scope::fork) .toList(); scope.join(); return futures.stream().map(Supplier::get).toList(); } } static <T> Callable<Future<T>> asFuture(Callable<T> task) { return () -> { try { return CompletableFuture.completedFuture(task.call()); } catch (Exception ex) { return CompletableFuture.failedFuture(ex); } }; }
  23. Fan-inシナリオ  外部からのTCPソケットを受け付ける例  Taskの生存期間は事前に決まっていない  Subtaskの個数も事前に決まっていない (動的に変化するため)  これまでの例

    (Fan-outシナリオ) だと...  Taskの生存期間は決まっている  Subtaskの個数も決まっている Subtask Subtask Subtask Subtask Task Peer
  24. StructuredTaskScope内でサブタスクをフォークし、着信接続を処理するサーバーの例 void serve(ServerSocket serverSocket) throws IOException, InterruptedException { try (var

    scope = new StructuredTaskScope<Void>()) { try { while (true) { var socket = serverSocket.accept(); scope.fork(() -> handle(socket)); } } finally { // エラーもしくは割り込み発生時には、Acceptを停止 scope.shutdown(); // 全アクティブ接続をクローズ scope.join(); } } }
  25. 経緯  JEP 429 (JDK 20)  Incubator  JEP

    446 (JDK 21)  java.langパッケージのPreview API
  26. 目指していること・いないこと 目指していること  スレッド内と子スレッドの両方でデータを共 有するプログラミング・モデルを提供し、デー タ・フローに関する推論を単純化する  コードの構文構造から共有データの生存 期間が見えるようにする 

    呼び出し元が共有するデータを、正当な 呼び出し元のみが取得できるようにする  共有データをImmutableなものとして扱い、 多数のスレッド間の共有を可能にし、実 行時の最適化を可能にする 目指していないこと  Javaプログラミング言語の変更  ThreadLocal変数からの移行や、既存 のThreadLocal APIの非推奨化
  27. ThreadLocalの課題 Mutable 無制限の ライフサイクル 高コストな 継承 • すべてのThread Local変数は変更可能 •

    どのコンポーネントがどのような順序で共有状態を更新しているのか追跡が大変 • 書き込んだデータはスレッドの寿命が尽きるまで、あるいはスレッドがremoveメソッ ドを呼び出すまで保持 • スレッドプール利用時、あるタスクで設定されたThreadLocal変数の値を適切 にクリアしなかったばかりに、無関係のタスクに誤ってリークしてしまう可能性... • ThreadLocal変数を継承する子スレッドでは、親スレッドで以前に書き込まれ たすべてのThreadLocal変数のストレージを割り当てる必要がある
  28. 基本的な使い方 final static ScopedValue<...> V = ScopedValue.newInstance(); // whereでScoped Valuesとバインド対象のオブジェクトを提示

    // runでScoped Valuesをバインド // run(...) が終了すると、バインディングは破棄 ScopedValue.where(V, <value>) .run(() -> { ... V.get() ... call methods ... }); // 直接的、もしくはラムダ式から間接的に呼び出されたメソッド内で値を取得可能 // set(...)メソッドは存在しないので、宣言時に書き込まれた値を取得できる ... V.get() ...
  29. ThreadLocalからScopedValueへの置き換え - ThreadLocalの場合 class Server { final static ThreadLocal<Principal> PRINCIPAL

    = new ThreadLocal<>(); void serve(Request request, Response response) { var level = (request.isAuthorized() ? ADMIN : GUEST); var principal = new Principal(level); PRINCIPAL.set(principal); Application.handle(request, response); } } class DBAccess { DBConnection open() { var principal = Server.PRINCIPAL.get(); if (!principal.canOpen()) throw new InvalidPrincipalException(); return newConnection(...); } }
  30. ThreadLocalからScopedValueへの置き換え - ScopedValueの場合 class Server { final static ScopedValue<Principal> PRINCIPAL

    = ScopedValue.newInstance(); void serve(Request request, Response response) { var level = (request.isAdmin() ? ADMIN : GUEST); var principal = new Principal(level); ScopedValue.where(PRINCIPAL, principal) .run(() -> Application.handle(request, response)); } } class DBAccess { DBConnection open() { var principal = Server.PRINCIPAL.get(); if (!principal.canOpen()) throw new InvalidPrincipalException(); return newConnection(...); } }
  31. Rebinding scoped values  以下の条件では、ネストされた呼び出しに対し、ScopedValue APIは新しい バインドを確立できる  条件 

    同じスレッド内  呼び出し元が同じScoped Valuesを使用  別の値を呼び出し元に伝える必要がある
  32. Rebinding scoped values (1/2) class Server { final static ScopedValue<Principal>

    PRINCIPAL = ScopedValue.newInstance(); void serve(Request request, Response response) { var level = (request.isAdmin() ? ADMIN : GUEST); var principal = new Principal(level); ScopedValue.where(PRINCIPAL, principal) .run(() -> Application.handle(request, response)); } } class Logger { void log(Supplier<String> formatter) { if (loggingEnabled) { var message = formatter.get(); write(logFile, "%s %s".format(timeStamp(), message)); } } } Logger.log(() -> { DBAccess.open();}) と記述できるが、formatter.get()では書式設定す るだけなので、不要な情報は渡したくない。
  33. Rebinding scoped values (2/2) class Server { final static ScopedValue<Principal>

    PRINCIPAL = ScopedValue.newInstance(); void serve(Request request, Response response) { var level = (request.isAdmin() ? ADMIN : GUEST); var principal = new Principal(level); ScopedValue.where(PRINCIPAL, principal) .run(() -> Application.handle(request, response)); } } class Logger { void log(Supplier<String> formatter) { if (loggingEnabled) { var guest = Principal.createGuest(); var message = ScopedValue.where(Server.PRINCIPAL, guest) .call(() -> formatter.get()); write(logFile, "%s %s".format(timeStamp(), message)); } } } .call(() -> formatter.get()) の範囲でのみ、Server.PRINCIPALとして guestを使う
  34. Scoped values as capabilities  Capability-based Security  ユーザアプリケーションを設計するためのコンセプトで、それらが「最小権限の原則」 (principle

    of least privilege) に基づいて直接 capability を分け合う方法でセキュリティを実現する方法  Scoped Valuesは、シンプルで堅牢なcapabilityの実装を提供  ScopedValueのオーナーは通常、private static finalのように適切にアクセス制限されたフィール ドで保護  ScopedValueオブジェクトは通常、広く共有されることはない(その必要もない)
  35. ユーザー名が定義されているコンテキストでのみ、特定の操作実行を強制する runAsUser(...)に渡されたRunnableの内部 で実行されているコードだけが、doOperation() を呼び出せる public class Framework { private Framework()

    {} private static final Framework INSTANCE = new Framework(); public static void Framework instance() { return INSTANCE; } private static final ScopedValue<String> USER = ScopedValue.newInstance(); public void runAsUser(String user, Runnable op) { ScopedValue.where(USER, user).run(op); } public void doOperation() { String user = USER.orElseThrow(() -> new IllegalStateException(“User not set”)); ... } }
  36. Scoped Valuesの継承 Webフレームワークの場合 リクエスト処理スレッド(親スレッド)上でユーザーコードが動作 ユーザーコードでは、Virtual Threadsを利用可(Virtual Threadsが子スレッド) このとき、リクエスト処理スレッドで動作するコンポーネントが共有するデータを子スレッドでも利用できる必要が ある 

    親スレッドのScoped Valueは、StructuredTaskScopeで作成された子ス レッドに自動的に継承される  子スレッドのコードは、親スレッドのScoped Valuesに対して確立されたバインディングを最小限のオーバーヘッ ドで使用可  ThreadLocal変数とは異なり、親スレッドのScoped Valuesバインディングは子スレッドにコピーされない
  37. Scoped Valuesの継承 (1/2) class Application { Response handle() throws ExecutionException,

    InterruptedException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Supplier<String> user = scope.fork(() -> findUser()); Supplier <Integer> order = scope.fork(() -> fetchOrder()); // 両Forkを待つ scope.join().throwIfFailed(); return new Response(user.get(), order.get()); } } String findUser() { ... DBAccess.open() ... } }
  38. Scoped Valuesの継承 (2/2) class Server { final static ScopedValue<Principal> PRINCIPAL

    = ScopedValue.newInstance(); void serve(Request request, Response response) { var level = (request.isAdmin() ? ADMIN : GUEST); var principal = new Principal(level); ScopedValue.where(PRINCIPAL, principal) .run(() -> Application.handle(request, response)); } } class DBAccess { DBConnection open() { var principal = Server.PRINCIPAL.get(); if (!principal.canOpen()) throw new InvalidPrincipalException(); return newConnection(...); } }
  39. Scoped Valueが有用なユースケース  リエントラントなコード  再帰の検出や制限が必要な場合  Nested transaction 

    再帰を検出すると、トランザクションの進行中に開始されたトランザクションは、すべて一番外側のトランザク ションの一部に変わる (flattened transactions)  Graphics context  プログラムのモジュール間で共有される描画コンテキスト
  40. Q&A