現象から見るJava AIOの本質|Dewu Technology

1 はじめに

Java BIO、NIO、および AIO の違いと原理に関するこのような記事は数多くありますが、それらは主に BIO と NIO の間で議論されており、AIO に関する記事はほとんどなく、それらの多くは紹介にすぎません。概念とコード例。

AIO について学習すると、次の現象に気付きました。

1. 2011年にJava 7がリリースされ、非同期IOと呼ばれるAIOというプログラミングモデルが追加されましたが、12年近くが経過し、ネットワークフレームワークNetty、Mina、Webコンテナなど、通常の開発フレームワークミドルウェアは依然としてNIOが支配しています。トムキャット、アンダートウ。

2. Java AIO は NIO 2.0 とも呼ばれますが、これも NIO ベースですか?

3. Netty は AIO のサポートを終了しました。https://github.com/netty/netty/issues/2515

4. AIOは問題を解決し、孤独を解放しただけのようです.
これらの現象は必然的に多くの人を混乱させるので、この記事を書くことにしたとき、単純に AIO の概念を繰り返すのではなく、現象を通して Java AIO の本質を分析し、考え、理解する方法を考えました。

2. 非同期とは

2.1 私たちが知っている非同期性

AIO の A は非同期を意味します AIO の原理を理解する前に、「非同期」とはどのような概念なのかを明確にしましょう。
非同期プログラミングといえば、次のコード例のように、通常の開発ではまだ比較的一般的です。

@Async
public void create() {
    //TODO
}
​
public void build() {
    executor.execute(() -> build());
}

@Async でアノテーションが付けられているか、タスクをスレッド プールにサブミットしているかに関係なく、それらはすべて同じ結果になります。つまり、実行するタスクを別のスレッドに渡して実行します。
このとき、いわゆる「非同期」がマルチスレッド化されてタスクを実行していると大まかに考えることができます。

2.2 Java BIO と NIO は同期ですか、非同期ですか?

Java BIO や NIO が同期であろうと非同期であろうと、まず非同期の考え方に従って非同期プログラミングを行います。

2.2.1 バイオの例

byte [] data = new byte[1024];
InputStream in = socket.getInputStream();
in.read(data);
// 接收到数据,异步处理
executor.execute(() -> handle(data));
​
public void handle(byte [] data) {
    // TODO
}

BIO read() の場合、スレッドはブロックされますが、データを受信すると、スレッドを非同期で開始して処理できます。

2.2.2 NIO の例

selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> iterator = keys.iterator();
while (iterator.hasNext()) {
    SelectionKey key = iterator.next();
    if (key.isReadable()) {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
        executor.execute(() -> {
            try {
                channel.read(byteBuffer);
                handle(byteBuffer);
            } catch (Exception e) {
​
            }
        });
​
    }
}
​
public static void handle(ByteBuffer buffer) {
    // TODO
}

同様に、NIO の read() はノンブロッキングですが、select() を介してデータの待機をブロックすることができます. 読み取るデータがある場合は、スレッドを非同期で開始してデータを読み取って処理します.

2.2.3 理解の逸脱

現時点では、Java の BIO と NIO が非同期であるか同期であるかは気分次第であると断言しますが、マルチスレッドでよければ非同期です。

でもこれだと、ブログ記事をたくさん読んで、基本的にはBIOとNIOがシンクロしていることが判明。

では、どこに問題があるのでしょうか? 何が私たちの理解の逸脱を引き起こしたのでしょうか?

それが基準系の問題です. 以前物理学を勉強したとき, バスの乗客が動いているか静止しているかを調べるには基準系が必要です. 地面を基準にすれば, 彼は動いており, バスは参照、彼は静止しています。

同じことが Java IO にも当てはまります. 同期か非同期かを定義するために参照システムが必要です. IO のモードがどのモードであるかを議論しているので、他の人が別の IO を開始している間に、IO の読み取りおよび書き込み操作を理解する必要があります.データを処理するスレッドは、既に IO の読み取りと書き込みの範囲外であり、関与すべきではありません。

2.2.4 async を定義しようとしている

そのため、IO の読み書き操作のイベントを参考に、まずIO の読み書きを開始するスレッド (読み書きを呼び出すスレッド) と、実際に IO の読み書きを操作するスレッドを定義してみます。それらは同じスレッドである場合、それを同期と呼び、それ以外の場合は非同期と呼びます

  • 明らかに、BIO は同期のみ可能です. in.read() を呼び出すと、現在のスレッドがブロックされます. データが返されると、元のスレッドがデータを受け取ります.

  • NIO も同期と呼ばれ、その理由は同じです. channel.read() を呼び出すと、スレッドはブロックされませんが、データを読み取るのは現在のスレッドです。

この考えによれば、AIO は IO の読み取りと書き込みを開始するスレッドである必要があり、実際にデータを受け取るスレッドは同じスレッドではない可能性があります. これは
事実ですか? では、Java AIO コードを開始しましょう。

2.3 Java AIO プログラム例

2.3.1 AIO サーバープログラム

public class AioServer {
​
    public static void main(String[] args) throws IOException {
        System.out.println(Thread.currentThread().getName() + " AioServer start");
        AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()
                .bind(new InetSocketAddress("127.0.0.1", 8080));
        serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {
​
            @Override
            public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {
                System.out.println(Thread.currentThread().getName() + " client is connected");
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                clientChannel.read(buffer, buffer, new ClientHandler());
            }
​
            @Override
            public void failed(Throwable exc, Void attachment) {
                System.out.println("accept fail");
            }
        });
        System.in.read();
    }
}
​
public class ClientHandler implements CompletionHandler<Integer, ByteBuffer> {
    @Override
    public void completed(Integer result, ByteBuffer buffer) {
        buffer.flip();
        byte [] data = new byte[buffer.remaining()];
        buffer.get(data);
        System.out.println(Thread.currentThread().getName() + " received:"  + new String(data, StandardCharsets.UTF_8));
    }
​
    @Override
    public void failed(Throwable exc, ByteBuffer buffer) {
​
    }
}

2.3.2 AIO クライアント プログラム

public class AioClient {
​
    public static void main(String[] args) throws Exception {
        AsynchronousSocketChannel channel = AsynchronousSocketChannel.open();
        channel.connect(new InetSocketAddress("127.0.0.1", 8080));
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        buffer.put("Java AIO".getBytes(StandardCharsets.UTF_8));
        buffer.flip();
        Thread.sleep(1000L);
        channel.write(buffer);
 }
}

2.3.3 非同期定義予想の結論

サーバー プログラムとクライアント プログラムを別々に実行する

640.png

サーバーを動かした結果、

メイン スレッドは、serverChannel.accept への呼び出しを開始し、コールバックを監視するために CompletionHandler を追加します。クライアントが接続すると、Thread-5 スレッドは、accept の完了したコールバック メソッドを実行します。

その直後、スレッド 5 は clientChannel.read 呼び出しを開始し、コールバックを監視する CompletionHandler を追加しました. データを受信すると、スレッド 1 は read の完了したコールバック メソッドを実行しました。

この結論は、上記の非同期の推測と一致しています. IO 操作 (受け入れ、読み取り、書き込みなど) を開始するスレッドは、最終的に操作を完了するスレッドと同じではありません. この IO モードを AIO と呼びます.

もちろん、このように AIO を定義するのはあくまで私たちの理解のためであり、実際には、非同期 IO の定義はより抽象的かもしれません。

3. AIO の例は思考の質問を促します

1. completed() メソッドを実行するスレッドを作成したのは誰で、いつ作成されましたか?

2. AIO 登録イベントの監視と実行コールバックを実装する方法は?

3. コールバックの監視の本質は何ですか?

3.1 質問 1: completed() メソッドを実行するスレッドの作成者と作成時期

通常、このような問題はプログラムの入り口から理解する必要がありますが、これはスレッドに関係しており、実際にはスレッドスタックの実行状況からスレッドがどのように実行されているかを突き止めることができます。

AIO サーバー プログラムのみを実行し、クライアントは実行せず、スレッド スタックを出力します (注: プログラムは Linux プラットフォームで実行され、他のプラットフォームはわずかに異なります)。

6401.png

スレッド スタックを分析し、プログラムが非常に多くのスレッドを開始することを確認します。

1. スレッド Thread-0 が EPoll.wait() メソッドでブロックされている

2. スレッド スレッド 1、スレッド 2。. . Thread-n (n は CPU コアの数と同じ) はブロッキング キューからタスクを取り、タスクが戻るのを待ってブロックします。

この時点で、次の結論を暫定的に導き出すことができます。

AIO サーバー プログラムが開始されると、これらのスレッドが作成され、スレッドはすべてブロックされた待機状態になります。

また、これらのスレッドの実行は Epoll に関連していることがわかりました.Epoll に関して言えば、Linux プラットフォームの下部に Java NIO が Epoll で実装されているという印象があります.Java AIO も Epoll で実装されていますか? この結論を確認するために、次の質問から議論します。

3.2 質問 2: AIO 登録イベントの監視と実行のコールバックを実装する方法

この問題を念頭に置いて、ソース コードを読んで分析したところ、ソース コードが非常に長く、ソース コードの解析は退屈なプロセスであり、読者を簡単に遠ざけてしまうことがわかりました。

長いプロセスと論理的に複雑なコードを理解するために、そのいくつかのコンテキストを把握し、どのコア プロセスを見つけることができます。

clientChannel.read(…) の例として、登録リスナー read を取り上げます。その主なコア プロセスは次のとおりです。

1. イベントを登録 → 2. イベントをリッスン → 3. イベントを処理

3.2.1 1. 登録イベント

6402.png

登録イベントは EPoll.ctl(…) 関数を呼び出し、この関数の最後のパラメーターを使用して、1 回限りか永続的かを指定します。上記のコード events | EPOLLONSHOT は文字通り、それが 1 回限りであることを意味します。

3.2.2 2. イベントの監視

6408.png

3.2.3 3. イベントの処理

6409.png

64010.png

64011.png

3.2.4 コアプロセスのまとめ

64012.png

上記のコード フローを分析すると、IO の読み取りと書き込みごとに経験する必要がある 3 つのイベントが 1 回限りであることがわかります。つまり、イベントが処理された後、このプロセスは終了します。 IO 読み書きするには、最初からやり直す必要があります。このように、いわゆるデス コールバック (次のコールバック メソッドがコールバック メソッドに追加される) が発生し、プログラミングの複雑さが大幅に増加します。

3.3 質問 3: コールバックを監視することの本質は何ですか?

先に結論から申し上げておきますと、いわゆるモニタリングコールバックの本質は、カーネルモード関数(正確にはread、write、epollWaitなどのAPI)を呼び出すユーザーモードスレッドです。返されない場合、ユーザー スレッドはブロックされます。関数が戻ると、ブロックされたスレッドが起動され、いわゆるコールバック関数が実行されます

この結論を理解するには、まずいくつかの概念を紹介する必要があります

3.3.1 システムコールと関数呼び出し

関数呼び出し:

関数を見つけて、その関数で関連するコマンドを実行する

システムコール:

オペレーティング システムは、ユーザー アプリケーションへのプログラミング インターフェイス、いわゆる API を提供します。

システムコール実行プロセス:

1. システムコールパラメータを渡す

2. システムコールは通常コアモードで実行する必要があるため、トラップされた命令を実行し、ユーザーモードからコアモードに切り替えます

3. システムコールプログラムの実行

4. ユーザー状態に戻る

3.3.2 ユーザーモードとカーネルモードの通信

ユーザーモード -> カーネルモード、システムコールのみ。

カーネルモード→ユーザーモード、カーネルモードはユーザーモードのプログラムが何の機能を持っているのか、パラメータは何なのか、アドレスはどこなのかを知りません。したがって、カーネルがユーザーモードで関数を呼び出すことは不可能であり、シグナルを送信することによってのみ. たとえば、プログラムを閉じる kill コマンドは、シグナルを送信することによってユーザープログラムを正常に終了させることです.

カーネル状態がユーザー状態でアクティブに関数を呼び出すことは不可能なので、なぜコールバックがあるのでしょうか? このいわゆるコールバックは、実際には自己主導型で自己実行型のユーザー状態であるとしか言えません。監視するだけでなく、コールバック関数も実行します。

3.3.3 実際の例で結論を検証する

この結論が説得力があるかどうかを検証するために、たとえば、コードの開発と記述に通常使用される IntelliJ IDEA は、マウスとキーボードのイベントをリッスンし、イベントを処理します。

慣習に従って、最初にスレッド スタックを出力すると、「AWT-XAWT」スレッドがマウスやキーボードなどのイベントの監視を担当し、「AWT-EventQueue」スレッドがイベント処理を担当することがわかります。

64013.png

特定のコードを見つけると、「AWT-XAWT」が while ループを実行し、waitForEvents 関数を呼び出してイベントが戻るのを待機していることがわかります。イベントがない場合、スレッドはそこでブロックされています。

64014.png

4. Java AIO の本質は何ですか?

1. カーネル モードはユーザー モード関数を直接呼び出すことができないため、Java AIO の本質は、ユーザー モードでのみ非同期を実装することです。理想的な意味での非同期性は達成されません。

理想的な非同期

理想的な意味での非同期性とは何ですか? ネットショッピングの一例です

消費者 A と配送業者 B の 2 つの役割

  • A さんがオンライン ショッピングをしているときに、自宅の住所を入力して支払い、注文を送信します。これは、監視イベントの登録に相当します。

  • 商人が商品を配達し、B が A のドアに商品を配達します。これはコールバックに相当します。

A さんは、オンラインで注文した後は、その後の配送プロセスについて心配する必要がなく、他のことを続けることができます。B は、A が家にいるかどうかを気にせず、商品を配達します. とにかく、家のドアに商品を投げてください. 2 人はお互いに依存せず、お互いに干渉しません.

A の買い物はユーザーモードで、B の速達はカーネルモードで行うと仮定すると、この種のプログラム動作モードは理想的すぎて、実際には実現できません。

現実の非同期性

Aさんは高級住宅街に住んでおり、自由に入ることができず、宅配便は住宅街の門までしか配達できません。

A は仕事で家にいなかったので、テレビなどの比較的重い製品を購入したので、友人 C にテレビを家に移動するのを手伝ってもらいました。
Aさんは出勤前に玄関先で警備員Dさんに「今日テレビが届く」と挨拶し、玄関にテレビが届いたらCさんに電話して取りに来てもらいます。

  • この時点で、A は注文を出し、D に挨拶します。これは、イベントの登録に相当します。AIO では EPoll.ctl(...) 登録イベントです。

  • 警備員がドアにしゃがむのは、イベントを聞いているのと同じ AIO では、Thread-0 スレッド EPoll.wait(…) を実行します。

  • 宅配便業者がテレビをドアに届けました。これは、IO イベントの到着に相当します。

  • 警備員が C にテレビが到着したことを知らせ、C がテレビを移動しに来ることは事件の処理に相当する。

AIO では、スレッド 0 がタスクをタスク キューに送信します。

Thread-1 ~n でデータをフェッチし、コールバック メソッドを実行します。

警備員 D は全過程で常にしゃがまなければならず、1 インチも離れることはできませんでした。

友人CもAの家に泊まらなければならない.彼は誰かから委託されているが,物が到着したときにその人がそこにいない.これは少し不誠実です.

したがって、実際の非同期性と理想的な非同期性は互いに独立しており、互いに干渉することはありません.この2つの点は相反する. セキュリティの役割は最大であり、これは彼の人生のハイライトの瞬間です。

イベントの登録、イベントのリッスン、イベントの処理、および非同期プロセスでのマルチスレッドの有効化、これらのプロセスのイニシエーターはすべてユーザー モードによって処理されるため、Java AIO はユーザー モードでのみ非同期を実装し、BIO で最初にブロックされます。と NIO 、ウェイクアップをブロックした後に非同期スレッド処理を開始する本質は同じです。

2. Java AIO は NIO と同じですが、各プラットフォームの基盤となる実装方法も異なり、Linux では EPoll、Windows では IOCP、Mac OS では KQueue が使用されます。原則は同じで、IO イベントをブロックして待機するユーザー スレッドと、キューからのイベントを処理するスレッド プールが必要です。

3. Netty が AIO を削除した理由は、パフォーマンスの点で AIO が NIO よりも高くないためです。Linux にも一連のネイティブ AIO 実装がありますが (Windows の IOCP に似ています)、Java AIO は Linux では使用されず、EPoll で実装されます。

4. Java AIO は UDP をサポートしていません

5.「デスコールバック」など、AIOのプログラミング方法がやや複雑

{{o.name}}
{{m.name}}

おすすめ

転載: my.oschina.net/u/5783135/blog/8570287
おすすめ