1. 序章

このチュートリアルでは、並行性の基本概念と、さまざまなプログラミング言語、特にJavaとKotlinがそれらにどのように対処するかについて説明します。

主に軽量の同時実行モデルに焦点を当て、KotlinのコルーチンをProjectLoomの一部としてJavaで予定されている提案と比較します。

2. 並行性の基本

並行性とは、プログラムを順序に依存しないコンポーネントまたは半順序のコンポーネントに分解する機能です。 ここでの目的は、結果に影響を与えることなく、複数の独立したプロセスを連携させることです。

オペレーティングシステムカーネル内では、プログラムのインスタンスをプロセスと呼びます。 カーネルは、セキュリティとフォールトトレランスのためにプロセスに異なるアドレス空間を割り当てることにより、プロセスを分離します。 各プロセスには独自のアドレス空間や開いているファイルハンドルなどがあるため、作成にはかなりの費用がかかります。

さらに、プロセスは互いのメモリにアクセスできないため、プロセス間通信は重要になります。

これは、カーネルレベルのスレッドが並行プログラミングの救済をもたらす場所です。

スレッドは、プロセス内の個別の実行行です。 プロセスは通常、複数のスレッドを持つことができます。 スレッドは同じファイルハンドルとアドレス空間を共有しますが、独自のプログラミングスタックを維持します。 これにより、スレッド間の通信がはるかに簡単になります。

オペレーティングシステムkernelは、カーネルレベルのスレッドを直接サポートおよび管理します。 カーネルは、外部からこれらのスレッドを作成および管理するためのシステムコールを提供します。 ただし、カーネルは、スケジューリングを含め、これらのスレッドを完全に制御できます。 これにより、カーネルレベルのスレッドが遅くなり、非効率になり、コストのかかるスレッド操作が発生します。

一方、実行中のアプリケーションに割り当てられたシステムメモリの一部であるユーザースペースでサポートされるユーザーレベルのスレッドもあります。

ユーザーレベルのスレッドを1対1または多対1のようなカーネルレベルのスレッドにマップするさまざまなモデルがあります。 ただし、仮想マシンのようなランタイムシステムは、ユーザーレベルのスレッドを直接管理します

カーネルはユーザーレベルのスレッドを認識しません。 したがって、ユーザーレベルのスレッドでのスレッド操作ははるかに高速です。 もちろん、これにはユーザーレベルのスレッドスケジューラとカーネル間の調整が必要です。

3. プログラミング言語の並行性

オペレーティングシステムが提供する同時実行プリミティブについて広く説明しました。 しかし、さまざまなプログラミング言語で利用可能な並行性の抽象化は、どのようにそれらを利用するのでしょうか。 詳細な分析はこのチュートリアルの範囲を超えていますが、ここでは一般的なパターンのいくつかについて説明します。

ほとんどの最新のプログラミング言語は並行プログラミングをサポートし、で動作する1つ以上のプリミティブを提供します。 たとえば、Javaは、Threadクラスと呼ばれる抽象化による並行性のファーストクラスのサポートを備えています。 これにより、Javaのスレッドにシステムに依存しない定義が提供されます。 ただし、内部的には、Javaはシステムコールを介してすべてのスレッドをカーネルレベルのスレッドにマップします。

すでに見てきたように、カーネルスレッドはプログラミングが簡単ですが、非常にかさばり、非効率的です。 実際、別の方法は、ユーザーレベルのスレッドを使用することです。 多くのプログラミング言語は、軽量スレッドの概念をネイティブにサポートしていますが、これを可能にする外部ライブラリもいくつかあります。

基本的なアプローチは、実行環境内でこれらの軽量スレッドのスケジューリングを処理することです。 ここでのスケジューリングは、プリエンプティブではなく協調的であるため、はるかに効率的です。 また、これらのスレッドをユーザースペースで管理するため、少数のカーネルスレッドでそれらを多重化して、カーネルスレッドの全体的なコストを削減できます。

プログラミング言語が異なれば、名前も異なります。 たとえば、Kotlin コルーチン、Golang コルーチン、Erlang プロセス、Haskellスレッドなどがあります。 。 Javaではそれらのネイティブサポートはありませんが、これは ProjectLoomの下でアクティブな提案に含まれています。 これらのいくつかについては、このチュートリアルの後半で説明します。

4. 並行性に向けた追加のアプローチ

これまでに説明した並行性モデルには共通点があり、プログラムのフローを同期的に推論することができます。 それらは非同期性を提供しますが、スレッドやコルーチンなどの基本的なプリミティブは、ほとんどそれを抽象化します。

ただし、より明示的な非同期プログラミングでは、この抽象化を破り、プログラムの一部を任意に実行できるようにします。

たとえば、リアクティブプログラミングは、まったく異なる視点で並行性を認識します。 プログラムフローを非同期的に発生する一連のイベントとして変換します。 したがって、プログラムコードは、これらの非同期イベントをリッスンし、それらを処理し、必要に応じて新しいイベントを公開する関数になります。

これらを大理石の図としてグラフィカルに表現することがよくあります。

さらに重要なことに、これらのイベントを公開またはサブスクライブするスレッドは、リアクティブプログラミングでは実際には重要ではありません。 炉心プロセスは限られた数のスレッドを使用し、通常は使用可能なCPUコアと一致します。 プールからの空きスレッドで関数を実行し、解放します。

したがって、ブロッキングコードの使用を回避できれば、単一のスレッドであっても、プログラムがはるかに効率的に実行される可能性があります。 また、他の非同期プログラミングスタイルに通常関連するコールバック地獄のようないくつかの問題点にも対処します。

ただし、を使用すると、プログラムの読み取りと書き込みの難易度が高くなり、のテストと保守が困難になります。

5. 構造化並行性の事例

複数の実行パスを持つ一般的な並行アプリケーションについては、推論するのが困難です。 問題の一部は、抽象化が欠けていることです。 たとえば、そのようなアプリケーションで関数を呼び出す場合、派閥が終了したときに処理が終了したことを保証することはできません。 これは、関数が複数の同時実行パスを生成した可能性があるためですが、そのパスは完全には認識されていません。

プログラムのシーケンシャルフローは、読み取りと書き込みがはるかに簡単です。 もちろん、並行性をサポートするには、このフローを分岐させる必要があります。 ただし、すべてのブランチがメインフローに戻って終了するかどうかを理解する方がはるかに簡単です。

したがって、抽象化を維持しながら、関数がプログラムを内部的にどのように分解するかはあまり気にしません。 これまでのところ、実行のすべての行が関数で終了するため、すべて問題ありません。 または、同時実行のスコープが適切にネストされます。 これは、構造化並行処理の基本的な前提です。 は、制御が同時タスクに分割された場合、それらは再び結合する必要があることを強調しています

リアクティブプログラミングのような非同期プログラミングモデルのいくつかを見ると、構造化された並行性を実現するのは難しいことがわかります。 実際、並行プログラミングは、スレッドのような単純なプリミティブを使用した場合でも、ほとんどの場合、任意のジャンプを伴います。

ただし、コルーチンのようなソリューションを使用して、Kotlinで構造化同時実行を実現できます。

このチュートリアルの後半で、その方法を説明します。

6. Kotlin:彼らはどのようにそれを行うのですか?

これで、Kotlinが並行性の問題を解決し、ほとんどの問題を回避する方法を調べるのに十分な背景を収集しました。 Kotlinは、2010年にJetBrainsによって開始されたオープンソースのプログラミング言語です。 Kotlinは、JavaScript、さらにはネイティブなどの他のプラットフォームとともにJVMをターゲットにしています。 したがって、Java互換のバイトコードを生成できます。

Kotlinは、コルーチンの形式で軽量スレッドのサポートを提供します。これは、リッチライブラリkotlinx.coroutinesとして実装されます。 興味深いことに、JVMは、コルーチンのような軽量の同時実行構造をネイティブでサポートしていません。少なくともまだです。 それにもかかわらず、Kotlinはかなり早い段階で実験的な言語機能としてコルーチンを導入し、バージョン1.3で公式になりました。

Kotlinがコルーチンを実装する方法と、コルーチンを使用して構造化同時実行の利点を備えた並行アプリケーションを作成する方法を説明します。

6.1. コルーチンとは正確には何ですか?

一般的に、コルーチンは、コンピュータープログラムまたは一般化されたサブルーチンの一部であり、任意の時点で実行を一時停止および再開できます。 これは、1950年代にアセンブリ言語のメソッドとして最初に登場しました。 コルーチンには、いくつかの興味深いアプリケーションがあります。

それらを同時実行に使用すると、カーネルスレッドに似ているように見えます。 ただし、微妙な違いがあります。 たとえば、スケジューラーはカーネルスレッドをプリエンプティブに管理しますが、コルーチンは自発的に制御を生成し、協調マルチタスクを実現します。

コルーチンの一般的な構造を見てみましょう。

coroutine
    loop
        while <em>some_condition</em>
            <em>some_action</em>
        yield

ここでは、ご覧のとおり、ループ内で何らかのアクションを実行するコルーチンがありますが、ブロックするのではなく、すべてのステップで協調的に制御を生成します。 これは、基盤となるカーネルスレッドをはるかに効率的に利用するのに役立ちます。 これは、リアクティブプログラミングのような他の非同期プログラミングスタイルとまったく同じですが、複雑さはありません。

コルーチンは特定のコルーチンに譲ることを選択できますが、複数のコルーチンをスケジュールするコントローラーが存在する場合もあります。 さらに興味深いことに、1つの基盤となるカーネルスレッドで数千のコルーチンを多重化できます。

しかし、その結果、コルーチンは、マルチコアシステムであっても、必ずしも並列処理を提供するとは限りません。

6.2. Kotlinコルーチンの動作

Kotlinは、 launch async、 runBlocking など、コルーチンを作成するための多くのコルーチンビルダーを提供しています。 さらに、Kotlinのコルーチンは常にコルーチンスコープにバインドされます。 コルーチンスコープにはコルーチンコンテキストが含まれ、コルーチンビルダーによって起動される新しいコルーチンスコープを設定します。

コルーチンを起動する方法についてはすぐに説明しますが、最初にサスペンド機能について理解しましょう。 Kotlinは、そのような関数をマークするためにsuspendと呼ばれる特別なキーワードを提供します。 これにより、コンパイラーはこれらの関数に魔法をかけることができます。これについては後で説明します。

一時停止関数を作成しましょう:

suspend fun doSomething() {
    // heavy computation here
}

このキーワードの使用を除けば、これらは単なる通常の関数であることがわかります。 ただし、重要な制限があります。サスペンド関数は、コルーチン内または別のサスペンド関数からのみ呼び出すことができます。

それでは、コルーチンビルダーの1つを使用してコルーチンを起動し、単純ですがサスペンド関数を呼び出しましょう。

GlobalScope.launch {
    doSomething() // does some heavy computation in the background
    ... do other stuff
}

ここでは、launchコルーチンビルダーを使用して新しいコルーチンを開始しています。

6.3. コルーチンとの構造化された並行性

ここで、は、正しい理由がない限り、GlobalScopeにバインドされたコルーチンを起動しないようにする必要があります。 これは、そのようなコルーチンがアプリケーションのライフサイクル全体で動作し、さらに重要なことに、構造化された同時実行の原則から逸脱しているためです。

構造化された同時実行性を順守するには、アプリケーション固有の CoroutineScope を作成し、そのインスタンスでコルーチンビルダーを使用する必要があります。

var job = Job()
val coroutineScope = CoroutineScope(Dispatchers.Main + job)

coroutineScope.launch { 
    doSomething() // does some heavy computation in the background 
    ... do other stuff 
}

CoroutineScope のインスタンスを作成するには、コルーチンを実行するスレッドを制御するDispatcherを定義する必要があります。 ここでのJobは、コルーチンのライフサイクル、キャンセル、および親子関係を担当します。

このCoroutineScopeを使用して起動されたすべてのコルーチンは、この親ジョブをキャンセルすることで簡単にキャンセルできます。 これにより、コルーチンが意図せずにリークするのを防ぎます。 これにより、サスペンド関数からコルーチンを起動するという副作用も回避されます。 したがって、前に説明した構造化同時実行を実現します。

6.4. フードの下を見る

したがって、今の問題は、Kotlinがコルーチンをどのように実装するかということです。 大まかに言えば、コルーチンは、サスペンションポイントと継続を備えた有限状態マシンとしてKotlinに実装されています。 この分野に慣れていない私たちにとって、これは意味がないかもしれません! ただし、簡単に説明します。

まず、紹介したばかりの用語のいくつかを理解しましょう。 一時停止ポイントは、実行を一時停止して後で再開する一時停止機能のポイントです。 同時に、継続とは、実際には、一時停止ポイントでの関数の状態のカプセル化です。 基本的に、継続は一時停止ポイントの後の残りの実行をキャプチャします。

これで、Kotlinはコンパイル時に、すべての一時停止関数を変換して、継続オブジェクトであるパラメーターを追加します。 コンパイラは、前のセクションのサスペンド関数のシグネチャを変換します。

fun doSomething(continuation: Continuation): Any?

このプログラミングスタイルは関数型プログラミングの典型であり、継続渡しスタイル(CPS)として知られています。 ここで、制御は継続の形式で明示的に渡されます。 これは、通知を受け取るためにコールバック関数を渡す非同期プログラミングスタイルにいくぶん似ています。 ただし、Kotlinのコルーチンでは、コンパイラーが継続を暗黙的に処理します。

Kotlinコンパイラは、サスペンド関数で可能なすべてのサスペンドポイントを識別し、サスペンドポイントで区切られたすべてのラベルを使用して状態を作成します。 結果の継続は、これらの状態をラベルとして持つ巨大なswitchステートメントに他なりません。

したがって、継続は、これを有限状態マシンとしてパックすることと考えることができます。

7. Java:提案は何ですか?

Javaは、その開始当初から、並行性を一流にサポートしてきました。 ただし、Javaは、軽量スレッドとして知られているものをネイティブでサポートしていません。 コアJavaの外部でこのようなサポートを構築する試みは何度かありましたが、どれも十分な成功を収めることができませんでした。

ここ数年、OpenJDKはこのギャップを埋めるためにProjectLoomに取り組んできました。

7.1. Javaでの並行性の簡単な歴史

JDK 1.0以降、クラスThreadは、Javaでの並行性のためのコア抽象化を提供しました。 これは、「一度書けばどこでも実行できる」という約束に一致するように、すべてのプラットフォームで同様に実行することを目的としていました。 残念ながら、一部のターゲットプラットフォームは、当時スレッドをネイティブでサポートしていませんでした。 したがって、Javaは、その約束を実現するために、グリーンスレッドと呼ばれるものを実装する必要がありました。

基本的に、グリーンスレッドは、ユーザースペースで管理され、仮想マシンによってスケジュールされるスレッドの実装です。 そのようなスレッドの一般的な定義をすでに見て、KotlinのコルーチンまたはGolangのコルーチンと同様の概念について説明しました。 グリーンスレッドは実装の点で異なる場合がありますが、基本的な考え方は実際には非常に似ていました。

当初、Javaはグリーンスレッドの実装を改良するのに苦労していました。 複数のプロセッサにグリーンスレッドをスケーリングすることは困難であったため、マルチコアシステムでの並列処理の恩恵を受けました。 この問題を回避し、並行プログラミングモデルを簡素化するために、Javaはバージョン1.3でグリーンスレッドを放棄することを決定しました。

そのため、 Javaは、すべてのスレッドを個別のネイティブカーネルスレッドにマップすることを決定しました。基本的に、JVMスレッドはオペレーティングシステムスレッドの薄いラッパーになりました。 これによりプログラミングモデルが簡素化され、Javaは、複数のコアにわたるカーネルによるスレッドのプリエンプティブスケジューリングによる並列処理の利点を活用できます。

7.2. Java同時実行モデルの問題

Javaの並行性モデルは実際には非常に使いやすく、ExecutorServiceCompletableFutureの導入により大幅に改善されました。 これも長期間うまくいきました。 ただし、の問題は、このモデルで作成された同時アプリケーションが、今日で前例のない規模に直面しなければならないことです。

たとえば、一般的なサーブレットコンテナは、リクエストごとのスレッドモデルで記述されています。 ただし、システム上に、処理する予定の同時リクエストの数と同じ数のスレッドを作成することは不可能です。 これには、本質的に非ブロッキングであるイベントループやリアクティブプログラミングなどの代替プログラミングモデルが必要ですが、それらには独自の問題があります。

7.3. プロジェクトルームの提案

今のところ、Javaが軽量スレッドのサポートを復活させる時が来たのではないかと推測するのは難しいことではありません。 これは実際にはProjectLoomの背後にある動機です。 このプロジェクトの目的は、Javaプラットフォームで軽量の同時実行モデルを調査およびインキュベートすることです。 アイデアは、JVMスレッドの上に軽量スレッドのサポートを構築し、基本的にJVMスレッドをネイティブカーネルスレッドから切り離すことです。

現在の提案は、JVMのレベルでいくつかのコア同時実行関連構造のサポートを導入することです。 これらには、仮想スレッド(以前はファイバーと呼ばれていました)、区切られた継続、および末尾呼び出しの除去が含まれます。スレッドの現在の構成は、基本的に継続とスケジューラーの構成です。 アイデアは、これらの懸念を分離し、これらのビルディングブロックの上に仮想スレッドをサポートすることです。

現在のJVMスレッドは、基盤となるカーネルスレッドの単なるラッパーであるため、継続とスケジューラの両方の実装を提供するためにカーネルに依存しています。 ただし、継続をJavaプラットフォーム内の構成として公開することにより、継続をグローバルスケジューラと組み合わせることができます。 これにより、JVM 内で完全に管理される軽量スレッドとして、仮想スレッドが発生します。

もちろん、Project Loomの背後にある考え方は、Javaの仮想スレッドのような構造を提供するだけでなく、それらが原因で発生する他の問題のいくつかに対処することでもあります。 たとえば、多数の仮想スレッド間でデータを渡すための柔軟なメカニズム。 構造化された同時実行に近い概念である、非常に多くの仮想スレッドを編成および監視するためのより直感的な方法。 または、現在のスレッドのスレッドローカルと同様に、非常に多くの仮想スレッドのコンテキストデータを管理します。

7.4. 継続を理解する

ProjectLoomのスコープ内で区切られた継続が実際に何を意味するのかを理解しましょう。 実際、区切られた継続の背後にある基本的な考え方は、すでに説明したコルーチンと何ら変わりはありません。 したがって、区切られた継続は、任意の時点で実行を一時停止し、同じポイントから再開できるシーケンシャルコードと見なすことができます。

ただし、Javaでは、提案は継続をパブリックAPIとして公開することです。 提案されたAPIは次のようになります。

class _Continuation {
    public _Continuation(_Scope scope, Runnable target) 
    public boolean run()
    public static _Continuation suspend(_Scope scope, Consumer<_Continuation> ccc)
    public ? getStackTrace()
}

継続は一般的な構成であり、仮想スレッドに固有のものではないことに注意してください。 仮想スレッドは実装のために継続を必要としますが、継続の他の可能な使用法もあります。 たとえば、これを使用してジェネレーターを実装できます。ジェネレーターは、単一の値を生成した後に生成されるイテレーターです。

7.5. 仮想スレッドの実装

Project Loomの焦点は、基本的な構成として仮想スレッドのサポートを提供することです。 仮想スレッドはJavaのユーザーモードスレッドの機能を提供するために提案された高レベルの構造です。 基本的に、仮想スレッドでは、実行を一時停止および再開する機能と同時に任意のコードを実行できるようにする必要があります。

すでに推測できるように、継続は仮想スレッドのような高レベルの構成を作成するために使用されます。 アイデアは、仮想スレッドが他の必要な部分とともに継続クラスのプライベートインスタンスを保持するということです。

class _VirtualThread {
    private final _Continuation continuation;
    private final Executor scheduler;
    private volatile State state;
    private final Runnable task;
​
    private enum State { NEW, LEASED, RUNNABLE, PAUSED, DONE; }
  
    public _VirtualThread(Runnable target, Executor scheduler) {
        .....
    }
  
    public void start() {
        .....
    }
  
    public static void park() {
        _Continuation.suspend(_FIBER_SCOPE, null);
    }
  
    public void unpark() {
        .....
    }
}

上記は、継続のように、低レベルのプリミティブを使用して仮想スレッドを構成する方法を簡単に表したものです。 また、スケジューラーは仮想スレッドの実装に不可欠な部分であることに注意してください。 ただし、仮想スレッドの初期のデフォルトのグローバルスケジューラは、Javaにすでに存在し、ワークスティーリングアルゴリズムを実装するForkJoinPoolになります。

さらに重要なことに、提案は仮想スレッドのAPIを現在のヘビーウェイトスレッドのAPIに非常に近づけることです。 現在存在する太い糸は今後も存在し続けます。 したがって、ヘビーウェイトまたは新しいライトウェイトスレッドがサポートするAPIの適合性は、ユーザーエクスペリエンスの向上につながります。

7.6. 現在の状態を覗き見

Project Loomはここ数年進行中であり、提案の一部は2021年にJava16の一部として利用可能になる可能性があります。 ただし、アーリーアクセスビルドは、新機能を試してフィードバックを提供するために、しばらくの間利用できます。

それで、最初に、重いスレッド、または現在私たちが知っているスレッドでの作業がどのように変化するかを見てみましょう。

Runnable printThread = () -> System.out.println(Thread.currentThread());
ThreadFactory kernelThreadFactory = Thread.builder().factory();
Thread kernelThread = kernelThreadFactory.newThread(printThread);
kernelThread.start();

ご覧のとおり、Thread.Builderと呼ばれる新しいインターフェイスがあります。これはThreadまたはThreadFactoryの可変ビルダーです。 これは、ここで行っているカーネルスレッド、または仮想スレッドの作成を容易にするためです。 他のすべては、今日存在するものと非常に似ています。

それでは、代わりに仮想スレッドを作成して使用する方法を見てみましょう。

Runnable printThread = () -> System.out.println(Thread.currentThread());
ThreadFactory virtualThreadFactory = Thread.builder().virtual().factory();
Thread virtualThread = virtualThreadFactory.newThread(printThread);
virtualThread.start();

仮想スレッドを作成するための別のスレッドファクトリがあるという事実を除けば、実際には違いはありません! これは、仮想スレッドの現在の実装では、新しいクラスが導入されておらず、Threadクラスの新しい実装のみが導入されているためです。

スレッドのこの新しい実装はスケジューリングが異なるという事実とは別に、それらに対して同じように機能しない他の側面があります。 たとえば、ThreadGroupThreadLocalなどの既存の構成要素の動作と影響は、仮想スレッドによって異なります。

8. Java仮想スレッドはKotlinコルーチンとどのように異なりますか?

Kotlinがコルーチンに関して持つ軽量の同時実行モデルのサポートと、Javaが仮想スレッドとして提供することを提案しているモデルについて詳しく説明しました。

明らかな問題は、それらを互いにどのように比較するかであり、同じJVMをターゲットにする場合に両方から利益を得ることができるかどうかです。 このセクションでは、継続やスケジューリングなどの重要な側面について説明します。

8.1. スタックフルvs。 スタックレス継続

継続はあらゆる形式のユーザーモードスレッド実装の基礎を形成するため、Kotlinでの実装と、Javaでの提案との違いを調べることから始めましょう。 デザインの選択について大まかに言えば、 Kotlinコルーチンはスタックレスですが、Javaでの継続はスタックフルであることが提案されています

名前が示すように、スタックフル継続またはコルーチンは、独自の関数呼び出しスタックを維持します。 ここでのスタックは、ローカル変数と関数の引数を格納するために必要な連続したメモリブロックです。 それどころか、スタックレスコルーチンはスタックを維持せず、呼び出し元に依存します。 これにより、発信者とのつながりが強くなります。

即時のフォールアウトとして、スタックレスコルーチンはトップレベルの関数からのみサスペンドできます。 したがって、コルーチンから呼び出されるすべての関数は、コルーチンを一時停止する前に終了する必要があります。 比較すると、スタックフル継続またはコルーチンは、呼び出しスタックの任意のネストされた深さで中断できます。 したがって、スタックフルコルーチンは、スタックレスコルーチンよりも強力で汎用的です。

ただし、スタックレスコルーチンはスタックフルコルーチンよりもメモリフットプリントが少ないため、より効率的であることが証明されています。 これは、スタックレスコルーチン間のコンテキスト切り替えがより安価になるためです。 さらに、コンパイラは、ランタイムからのサポートがほとんどない状態で、スタックレスコルーチンのコード変換をローカルで処理します。

8.2. プリエンプティブvs。 協調スケジューリング

継続とは別に、軽量スレッドの実装のもう1つの重要な部分はスケジューリングです。 オペレーティングシステムスケジューラがカーネルスレッドをプリエンプティブにスケジュールする方法を見てきました。 実際、これがカーネルスレッドが非効率であることが判明する理由の1つです。 したがって、通常、軽量スレッドをスケジューリングするためのアプローチは、任意よりも構造化されています。

前に見たように、Kotlinコルーチンでのスケジューリングは協調的であり、コルーチンは論理ポイントで自発的に制御を生成します。 たとえば、計算量の多い操作やブロック操作をサスペンド関数でラップすることを決定できます。 コルーチンまたは別のサスペンド関数からこのような関数を呼び出すと、これらは自然なサスペンドポイントになります。

ただし、Javaでの現在の提案は、スケジューリングを協調的ではなくプリエンプティブに保つことです。 したがって、Java仮想スレッドで一時停止ポイントを定義することはできません。 それで、それはカーネルスケジューラの負担を負うことを意味しますか? あまり。 カーネルスレッドは、タイムスライスの概念に基づいて任意にプリエンプトされることに注意してください。

ただし、 Javaでの仮想スレッドスケジューラの提案は、I/Oまたは同期でブロックするときにそれらをプリエンプトすることです

スケジュール方法に関係なく、軽量スレッドは最終的に基盤となるカーネルスレッドで実行されます。 Kotlinコルーチンの場合、コルーチンコンテキストにはコルーチンディスパッチャーが含まれます。 コルーチンディスパッチャは、コルーチンが実行に使用するカーネルスレッドを決定します。

一方、Java仮想スレッドスケジューラは、カーネルスレッドのプールをワーカーとして維持し、実行可能な仮想スレッドを使用可能なワーカーの1つにマウントします。

9. 結論

このチュートリアルでは、並行性の基本的な概念と、軽量の並行性が重量のある並行性とどのように異なるかを理解しました。 また、プログラミング言語で並行性が一般的にどのようにアプローチされているか、および構造化並行性が何を意味するかについても触れました。

さらに、コルーチンとしてKotlinで軽量の並行性がどのようにサポートされているか、およびJavaがその点で仮想スレッドの導入をどのように提案しているかを理解しました。 これらの構成について詳細に説明した後、それらの実装が互いにどのように異なるかについて触れました。