Javaコルーチン実践ガイド(1)

1.コルーチン生成の背景

コルーチンについて言えば、ほとんどの人の第一印象はGoLangである可能性があります。これは、組み込みの同時実行サポートであるGo言語の非常に魅力的な側面の1つでもあります。Go言語の並行性システムの理論は、1978年にCAR Hoareによって提案されたCSP(Communicating Sequential Process)です。CSPは正確な数学的モデルを持っており、実際にはHoareによって設計されたT9000汎用コンピューターに適用されます。NewSqueak、Alef、Limboから現在のGo言語まで、CSPで20年以上の実務経験を持つRob Pikeにとって、彼はCSPを汎用プログラミング言語に適用する可能性についてより懸念しています。Goの並行プログラミングのコアであるCSP理論のコアコンセプトは、同期通信の1つだけです。

まず第一に、概念は明確でなければなりません:並行性は並列性ではありません。並行性は、プログラムの設計レベルに関係します。並行プログラムは順番に実行でき、実際のマルチコアCPUでのみ同時に実行できます。並列処理は、プログラムの実行レベルに関係します。並列処理は、一般に単純で、繰り返し回数が多くなります。たとえば、GPUでの画像処理には、多数の並列操作があります。同時プログラムをより適切に作成するために、Go言語は、設計の当初から、プログラミング言語レベルで簡潔で安全かつ効率的な抽象モデルを設計する方法に重点を置いており、プログラマーは問題の分解とソリューションの組み合わせに集中できます。スレッド管理とシグナルの相互作用の影響を受けます。これらの面倒な操作を避けて、エネルギーをそらしてください。

並行プログラミングでは、共有リソースへの正しいアクセスには正確な制御が必要です。現在、ほとんどの言語では、この難しい問題はロックなどのスレッド同期スキームによって解決されていますが、Go言語は別のアプローチを採用しています。チャネルを介して渡されます(実際には、複数の独立して実行されているスレッドがアクティブにリソースを共有することはめったにありません)。いつでも、できれば1人のGoroutineだけがリソースを所有できます。データの競合は設計レベルから排除されます。この考え方を促進するために、Goは並行プログラミング哲学をスローガンに翻訳しました。

メモリを共有して通信するのではなく、通信してメモリを共有してください。

これは、より高いレベルの並行プログラミング哲学です(Goでは、パイプを介して値を渡すことが推奨される方法です)。参照カウントのような単純な同時実行の問題は、不可分操作やミューテックスでは問題ありませんが、チャネルを使用してアクセスを制御すると、より簡潔で正しいプログラムを作成できます。

7週間の7つの並行性モデルで説明されている7つの並行プログラミングモデル。

参照:www.cnblogs.com/barrywxx/p/…

  1. スレッドとロック:スレッドとロックのモデルには多くのよく知られた欠点がありますが、それでも他のモデルの技術的基盤であり、多くの並行ソフトウェア開発の最初の選択肢です。

  2. 関数型プログラミング:関数型プログラミングがより重要になっている理由の1つは、並行プログラミングと並列プログラミングを適切にサポートすることです。関数型プログラミングは可変状態を排除するため、基本的にスレッドセーフであり、並列実行が容易です。

  3. Clojureの方法-アイデンティティと状態の分離:プログラミング言語Clojureは、両方の長所を活用するために微妙なバランスをとる、必須の関数型プログラミングのマッシュアップです。

  4. アクター:アクターモデルは、広く適用可能な並行プログラミングモデルであり、共有メモリモデルと分散メモリモデルに適しています。また、地理的に分散した問題を解決し、強力なフォールトトレランスを提供します。

  5. Communicating Sequential Processes(CSP):表面的には、CSPモデルはアクターモデルと非常によく似ており、どちらもメッセージパッシングに基づいています。ただし、CSPモデルは情報を送信するためのチャネルに焦点を当てていますが、アクターモデルはチャネルの両端のエンティティに焦点を当てており、CSPモデルを使用するコードのスタイルは大幅に異なります。

  6. データレベルの並列処理:すべてのラップトップの内部にはスーパーコンピューター、つまりGPUがあります。GPUは、データレベルの並列処理を利用して、高速な画像処理だけでなく、より広いフィールドにも対応します。有限要素解析、流体力学計算、またはその他の大量の数値計算を実行する場合は、GPUのパフォーマンスが最適です。

  7. ラムダアーキテクチャ:ビッグデータの時代の到来は並列処理と切り離せません。テラバイトのデータを処理できるようにするには、コンピューティングリソースを増やすだけで済みます。Lambdaアーキテクチャは、MapReduceとストリーム処理の特性を組み合わせたものであり、さまざまなビッグデータの問題を処理できるアーキテクチャです。

一般的な言語には、次の並行性モデルがあります。

  • スレッドモデル

    オペレーティングシステムの抽象化、高い開発効率、IO集約型、高い同時実行性の下での高いスイッチングオーバーヘッド。

  • 非同期モデル

    编程框架抽象,执行效率高,破坏结构化编程,开发门槛高。

  • 协程模型

    语言运行时抽象,轻量级线程,兼顾开发效率和执行效率。

二. Java协程发展历程

Java本身有着丰富的异步编程框架,比如说CompletableFuture,在一定程度上缓解了Java使用协程的紧迫性。

在2010年,JKU大学发表了一篇论文《高效的协程》,向OpenJdk社区提了一个协程框架的Patch,在2013年Quasar和Coroutine,这两种协程框架不需要修改Runtime,在协程切换时本来是要保存调用栈的,但是它们不保存这个调用栈,而是在切换时回溯调用链,生成一个状态机,将状态机保存起来。

Quasar和Coroutine并不是OpenJdk社区原生的协程解决方案,直到2018年1月,官方提出了Project Loom,到了2019年,Loom的首个EA版本问世,此时Java的协程类叫做Fiber,但社区觉得这引入了一个新的概念,于是在2019年10月将Fiber重新实现为了Thread的子类VirtualThread,兼容Thread的所有操作。

这时Project Loom的基本雏形已经完成了,在它的概念中,协程就是一个特殊的线程,是线程的一个子类,从Project Loom已经可以看到Open Jdk社区未来协程发展的方向, 但Loom还有很多的工作需要完成,并没有完全开发完。

三. Project Loom的目标与挑战

  • 目标

    易于理解的Java协程系统解决方案,协程即线程。

Virtual threads are just threads that are scheduled by the Java virtual machine rather than the operating system.

  • 挑战

    兼容庞大而复杂的标准类库、JVM特性,同时支持协程和线程。

四. Loom实现架构

在API层面Loom引入最重要的概念就是Virtual Thread,对于使用者来说可以当做Thread来理解。

下面是协程生命周期的描述,与线程相同需要一个start函数开始执行,接下来VirtualThread就会被调度执行,与线程不同的是,协程的上层需要一个调度器来调度它,而不是被操作系统直接调度,被调度执行后就是执行业务代码,此时我们业务代码可能会遇到一个数据库访问或者IO操作,这时当前协程就会被Park起来,与线程相同,此时我们的协程需要在切换前保存上下文,这步操作是由Runtime的Freeze来执行,等到IO操作完成,协程被唤醒继续执行,这时就要恢复上下文,这一步叫做Thaw。

1. Freeze操作

上图左侧是对Freeze的介绍,首先一个协程要被执行需要一个调度器,在Java生态本身就有一个非常不错的调度器ForkJoinPool,Loom也默认使用ForkJoinPool来作为调度器。

图中ForkJoinWorkerThread调用栈前半部分直到enterSpecial都是类库的调用栈,用户不需要考虑,A可以理解为用户自己的实现,从函数A调用到函数B,函数B调用函数C,函数C此时有一个数据访问,就会将当前协程挂起,yield操作会去保存当前协程的执行上下文,调用freeze,freeze会做一个stack walk,从当前调用栈的最后一层(yield)回溯到用户调用(函数A),将这些内容拷贝到一个stack。这也是协程栈大小不固定的原因,我们可以动态扩缩协程需要的空间,而线程栈大小默认1M,不管用没用到。而协程按需使用的特点,可以创建的数量非常多。extract_pop是Loom非常好的一个优化,它将ABC调用栈中的Java对象单独拷贝到一个refStack,在GC root时,如果把协程栈也当做root,几百万个协程会导致扫描停顿很久,Loom将所有对象都提到一个refStack里面,只需要处理这个stack即可,避免过多的协程栈增加GC时间。

2. Thaw操作

解凍は実行を再開するために使用されます。実行スタックが非常に深い可能性があるため、すべてのABCをコピーしてスタック内のyieldを実行スタックに戻すには時間がかかる場合があります。調査の結果、Loomコミュニティのメンバーは関数Cにさらに多くの機能がある可能性があることを発見しました。複数のデータアクセス操作。、実行スタックが復元された後、CのIO操作によりコンテキストが再び切り替わる可能性があるため、Loomはレイジーコピー方式を使用し、一度に一部のみreturn barrierをコピーして、コピーを続行します。実行が完了した後のスタック。このようにして、比較的大きい最初のスイッチングオーバーヘッドを除いて、他のすべてのスイッチングオーバーヘッドは小さくなります。

一方、refStackに保存されているOOPは、実行中に多くのGCがOOPアドレスを変更する可能性があり、復元しないとアクセスに問題が発生する可能性があるため、復元する必要があります。

5.織機の使用

  • 仮想スレッドの作成

    • Thread.builderを介してVirtualThreadを作成します

    • Thread.builderを介してVirtualThreadファクトリを作成します

    • デフォルトのForkJoinPoolスケジューラー(負荷分散、自動拡張)、カスタムスケジューラーをサポート

  • カスタムスケジューラ
static ExecutorService SCHEDULER_1 = Executors.newFixedThreadPool(1);
Thread thread = Thread.ofVirtual().scheduler(SCHEDULER_1).start(() -> System.out.println("Hello"));
thread.join();
复制代码
  • コルーチンプールを作成する
ThreadFactory factory;
if (usrFiber == false) {
    factory = Thread.builder().factory();
} else {
    factory = Thread.builder().ofVirtual().factory();
}
ExecutorService e = Executors.newFixThreadPool(threadCount, factory);
for (int i=0; i < requestCount; i++) {
    e.execute(r);
}
复制代码

おすすめ

転載: juejin.im/post/6974216114318508046