Mire la esencia de Java AIO a través del fenómeno | Tecnología Dewu

1. Introducción

Hay muchos artículos sobre las diferencias y los principios de Java BIO, NIO y AIO, pero se discuten principalmente entre BIO y NIO, mientras que hay muy pocos artículos sobre AIO, y muchos de ellos son solo introducciones. los conceptos y ejemplos de código.

Al aprender sobre AIO, se han notado los siguientes fenómenos:

1. Java 7 se lanzó en 2011, que agregó un modelo de programación llamado AIO llamado IO asíncrono, pero han pasado casi 12 años, y el middleware de marco de desarrollo habitual todavía está dominado por NIO, como el marco de red Netty, Mina, contenedor web Tomcat, resaca.

2. Java AIO también se llama NIO 2.0. ¿También está basado en NIO?

3. Netty abandonó el soporte de AIO. https://github.com/netty/netty/issues/2515

4. AIO parece haber resuelto solo el problema y liberado una soledad.
Estos fenómenos inevitablemente confundirán a muchas personas, así que cuando decidí escribir este artículo, no quería simplemente repetir el concepto de AIO, sino cómo analizar, pensar y comprender la esencia de Java AIO a través del fenómeno.

2. Qué es asíncrono

2.1 Asincronía tal como la conocemos

La A de AIO significa Asincrónico Antes de entender el principio de AIO, aclaremos qué tipo de concepto es "asincrónico".
Hablando de programación asíncrona, todavía es relativamente común en el desarrollo normal, como los siguientes ejemplos de código:

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

Ya sea que se anote con @Async o envíe tareas al grupo de subprocesos, todos terminan con el mismo resultado, que es entregar la tarea a ejecutar a otro subproceso para su ejecución.
En este momento, se puede considerar aproximadamente que el llamado "asincrónico" tiene múltiples subprocesos y ejecuta tareas.

2.2 ¿Java BIO y NIO son síncronos o asíncronos?

Ya sea que Java BIO y NIO sean sincrónicos o asincrónicos, primero hacemos programación asincrónica de acuerdo con la idea de asincronía.

2.2.1 Ejemplo de BIO

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

Cuando BIO read(), aunque el subproceso está bloqueado, al recibir datos, se puede iniciar un subproceso de forma asíncrona para procesar.

2.2.2 Ejemplo 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
}

De la misma manera, aunque NIO read() no bloquea, puede bloquear la espera de datos a través de select(). Cuando hay datos para leer, inicia un hilo de forma asíncrona para leer y procesar datos.

2.2.3 Desviaciones en la comprensión

En este momento, juramos que si el BIO y el NIO de Java son asíncronos o síncronos depende de su estado de ánimo. Si está feliz de darle un subproceso múltiple, es asíncrono.

Pero si este es el caso, después de leer muchos artículos del blog, básicamente se aclara que BIO y NIO están sincronizados.

Entonces, ¿dónde está el problema?, ¿qué causó la desviación en nuestro entendimiento?

Ese es el problema del marco de referencia. Al estudiar física antes, ya sea que los pasajeros del autobús estén en movimiento o estacionarios, se requiere un marco de referencia. Si el suelo se usa como referencia, él se está moviendo y el autobús se usa como referencia. una referencia, es estacionario.

Lo mismo es cierto para Java IO. Se necesita un sistema de referencia para definir si es síncrono o asíncrono. Ya que estamos discutiendo qué modo de IO es, es necesario comprender las operaciones de lectura y escritura de IO, mientras que otros inician otro. Los subprocesos para procesar datos ya están fuera del alcance de la lectura y escritura de E/S y no deberían estar involucrados.

2.2.4 Tratando de definir asíncrono

Por lo tanto, tomando como referencia el evento de las operaciones de lectura y escritura de E/S, primero tratamos de definir el subproceso que inicia la lectura y escritura de E/S (el subproceso que llama a la lectura y escritura) y el subproceso que realmente opera la lectura y escritura de E/S. son el mismo hilo, luego llámelo sincrónico, de lo contrario asincrónico .

  • Obviamente, BIO solo puede ser síncrono. Llamar a in.read() bloquea el hilo actual. Cuando se devuelven datos, el hilo original recibe los datos.

  • Y NIO también se llama sincronización, y la razón es la misma: al llamar a channel.read(), aunque el subproceso no se bloqueará, sigue siendo el subproceso actual el que lee los datos.

De acuerdo con esta idea, AIO debería ser el subproceso que inicia la lectura y escritura de IO, y el subproceso que realmente recibe los datos puede no ser el mismo subproceso. ¿Es este
el caso? Empecemos el código Java AIO ahora.

2.3 Ejemplo de programa Java AIO

2.3.1 Programa de servidor todo en uno

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 Programa cliente 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 Conjetura de definición asincrónica Conclusión

Ejecute los programas de servidor y cliente por separado

640.png

Como resultado de ejecutar el servidor,

El subproceso principal inicia una llamada a serverChannel.accept y agrega un CompletionHandler para monitorear la devolución de llamada. Cuando un cliente se conecta, el subproceso Thread-5 ejecuta el método de devolución de llamada completado de aceptación.

Inmediatamente después, Thread-5 inició la llamada clientChannel.read y agregó un CompletionHandler para monitorear la devolución de llamada. Al recibir datos, Thread-1 ejecutó el método de lectura de devolución de llamada completado.

Esta conclusión es consistente con la conjetura asíncrona anterior. El subproceso que inicia la operación de E/S (como aceptar, leer, escribir) no es el mismo que el subproceso que finalmente completa la operación. A esto lo llamamos modo de E/S AIO .

Por supuesto, definir AIO de esta manera es solo para nuestra comprensión. En la práctica, la definición de IO asíncrono puede ser más abstracta.

3. El ejemplo AIO genera preguntas para pensar

1. ¿Quién creó el subproceso que ejecuta el método complete() y cuándo se creó?

2. ¿Cómo implementar el monitoreo de eventos de registro AIO y la devolución de llamada de ejecución?

3. ¿Cuál es la esencia de monitorear la devolución de llamada?

3.1 Pregunta 1: Quién creó el subproceso que ejecuta el método complete() y cuándo se creó

Generalmente, tal problema debe entenderse desde la entrada del programa, pero está relacionado con el hilo. De hecho, es posible ubicar cómo se ejecuta el hilo desde el estado de ejecución de la pila de hilos.

Ejecute solo el programa del servidor AIO, el cliente no se ejecuta, imprima la pila de subprocesos (Nota: el programa se ejecuta en la plataforma Linux y otras plataformas son ligeramente diferentes)

6401.png

Analice la pila de subprocesos y descubra que el programa inicia tantos subprocesos

1. Thread Thread-0 está bloqueado en el método EPoll.wait()

2. Hilo Hilo-1, Hilo-2. . . Thread-n (n es lo mismo que el número de núcleos de la CPU) toma () tareas de la cola de bloqueo y bloquea la espera de que regrese una tarea.

En este punto, se puede extraer tentativamente la siguiente conclusión:

Una vez que se inicia el programa del servidor AIO, se crean estos subprocesos y todos los subprocesos se encuentran en un estado de espera bloqueado.

Además, descubrí que la ejecución de estos subprocesos está relacionada con Epoll. Cuando se trata de Epoll, tenemos la impresión de que Java NIO se implementa con Epoll en la parte inferior de la plataforma Linux. ¿Java AIO también se implementa con Epoll ? Para confirmar esta conclusión, discutimos a partir de la siguiente pregunta

3.2 Pregunta 2: Cómo implementar la devolución de llamada de ejecución y supervisión de eventos de registro AIO

Con este problema en mente, cuando leí y analicé el código fuente, descubrí que el código fuente es muy largo y que el análisis del código fuente es un proceso aburrido que puede alejar fácilmente a los lectores.

Para la comprensión de un proceso largo y un código lógicamente complejo, podemos comprender sus diversos contextos y descubrir qué procesos centrales.

Tome la lectura del oyente de registro como ejemplo clientChannel.read(…), su principal proceso central es:

1. Registrar evento -> 2. Escuchar evento -> 3. Procesar evento

3.2.1 1. Evento de registro

6402.png

El evento de registro llama a la función EPoll.ctl(…), y el último parámetro de esta función se usa para especificar si es único o permanente. Los eventos de código anteriores | EPOLLONSHOT significan literalmente que es único.

3.2.2 2. Supervisar eventos

6408.png

3.2.3 3. Manejo de eventos

6409.png

64010.png

64011.png

3.2.4 Resumen de los procesos centrales

64012.png

Después de analizar el flujo de código anterior, encontrará que los tres eventos que se deben experimentar para cada lectura y escritura de E/S son únicos, es decir, después de que se procesa el evento, este proceso finaliza. Si desea continuar con el siguiente IO Para leer y escribir, hay que empezar todo de nuevo. De esta manera, habrá una llamada devolución de llamada de muerte (el siguiente método de devolución de llamada se agrega al método de devolución de llamada), lo que aumenta en gran medida la complejidad de la programación.

3.3 Pregunta 3: ¿Cuál es la esencia de monitorear las devoluciones de llamadas?

Permítanme hablar sobre la conclusión primero. La esencia de la llamada devolución de llamada de monitoreo es el subproceso de modo de usuario, que llama a la función de modo kernel (hablando con precisión, API, como lectura, escritura, epollWait). no devuelto, el subproceso de usuario está bloqueado. Cuando la función regresa, el hilo bloqueado se despierta y se ejecuta la llamada función de devolución de llamada .

Para entender esta conclusión, primero debemos introducir varios conceptos

3.3.1 Llamadas al sistema y llamadas a funciones

Llamada de función:

Encuentre una función y ejecute comandos relacionados en la función

Llamada al sistema:

El sistema operativo proporciona una interfaz de programación para las aplicaciones del usuario, la llamada API.

Proceso de ejecución de llamadas al sistema:

1. Pase los parámetros de llamada al sistema

2. Ejecute instrucciones atrapadas, cambie del modo de usuario al modo central, porque las llamadas al sistema generalmente deben ejecutarse en modo central

3. Ejecute el programa de llamada al sistema

4. Volver al estado de usuario

3.3.2 Comunicación entre modo usuario y modo kernel

Modo de usuario -> Modo kernel, solo a través de llamadas al sistema.

Modo kernel -> modo usuario, el modo kernel no sabe qué funciones tiene el programa en modo usuario, cuáles son los parámetros y dónde está la dirección. Por lo tanto, es imposible que el kernel llame a funciones en el modo de usuario, pero solo mediante el envío de señales.Por ejemplo, el comando kill para cerrar el programa es permitir que el programa del usuario salga correctamente mediante el envío de señales.

Dado que es imposible que el estado del kernel llame activamente a funciones en el estado de usuario, ¿por qué hay una devolución de llamada? Solo se puede decir que esta llamada devolución de llamada es en realidad un estado de usuario autodirigido y autoejecutado. No solo supervisa, sino que también ejecuta la función de devolución de llamada.

3.3.3 Verificar la conclusión con ejemplos prácticos

Para verificar si esta conclusión es convincente, por ejemplo, IntelliJ IDEA, que generalmente se usa para desarrollar y escribir código, escucha eventos de mouse y teclado y maneja eventos.

De acuerdo con la convención, primero imprima la pila de subprocesos y encontrará que el subproceso "AWT-XAWT" es responsable de monitorear eventos como el mouse y el teclado, y el subproceso "AWT-EventQueue" es responsable del procesamiento de eventos.

64013.png

Al ubicar el código específico, puede ver que "AWT-XAWT" está haciendo un bucle while, llamando a la función waitForEvents para esperar a que regrese el evento. Si no hay evento, el hilo ha sido bloqueado allí.

64014.png

4. ¿Cuál es la esencia de Java AIO?

1. Dado que el modo kernel no puede llamar directamente a las funciones del modo usuario, la esencia de Java AIO es implementar la asincronía solo en el modo usuario. No logra la asincronía en el sentido ideal.

asíncrono ideal

¿Qué es la asincronía en el sentido ideal? Aquí hay un ejemplo de compras en línea.

Dos roles, consumidor A y mensajero B

  • Cuando A está comprando en línea, complete la dirección de su casa para pagar y enviar el pedido, lo que equivale a registrar el evento de monitoreo

  • El comerciante entrega los bienes y B entrega el artículo en la puerta de A, lo que equivale a una devolución de llamada.

Después de que A realiza el pedido en línea, no necesita preocuparse por el proceso de entrega posterior y puede continuar con otras cosas. A B no le importa si A está en casa o no cuando entrega la mercancía. De todos modos, simplemente tira la mercancía a la puerta de la casa. Las dos personas no dependen una de la otra y no interfieren entre sí .

Suponiendo que la compra de A se realiza en modo de usuario y la entrega rápida de B se realiza en modo kernel, este tipo de modo de operación del programa es demasiado ideal y no se puede realizar en la práctica.

Asincronía en la realidad

A vive en una zona residencial de alto nivel y no puede entrar a voluntad, y el servicio de mensajería solo puede ser entregado en la puerta de la zona residencial.

A compró un producto relativamente pesado, como un televisor, porque A iba a trabajar y no estaba en casa, por lo que le pidió a un amigo C que lo ayudara a trasladar el televisor a su casa.
Antes de que A se vaya al trabajo, saluda al guardia de seguridad D en la puerta y le dice que hoy se entregará un televisor. Cuando lo entreguen en la puerta de la comunidad, llame a C y pídale que venga a recogerlo.

  • En este punto, A hace un pedido y saluda a D, lo que equivale a registrar un evento. En AIO es el evento de registro EPoll.ctl(...).

  • El guardia de seguridad en cuclillas en la puerta equivale a escuchar el evento. En AIO, es el hilo Thread-0. Do EPoll.wait (…)

  • El mensajero entregó el televisor en la puerta, lo que equivale a la llegada de un evento IO.

  • El guardia de seguridad notifica a C que el televisor ha llegado y C viene a mover el televisor, lo que equivale a manejar el incidente.

En AIO, Thread-0 envía tareas a la cola de tareas.

Thread-1 ~n para obtener datos y ejecutar el método de devolución de llamada.

Durante todo el proceso, el guardia de seguridad D tuvo que agacharse todo el tiempo y no podía salir ni un centímetro, de lo contrario, le robarían el televisor cuando lo entregaron en la puerta.

El amigo C también tiene que quedarse en la casa de A. Alguien le confía, pero la persona no está cuando llegan las cosas. Esto es un poco deshonesto.

Por lo tanto, la asincronía actual y la asincronía ideal son independientes entre sí y no interfieren entre sí, estos dos puntos son contrarios entre sí . El papel de la seguridad es el más grande, y este es el momento más destacado de su vida.

Al registrar eventos, escuchar eventos, procesar eventos y habilitar subprocesos múltiples en el proceso asincrónico, los iniciadores de estos procesos son manejados por el modo de usuario, por lo que Java AIO solo implementa la asincronía en el modo de usuario, que se bloquea primero con BIO. y NIO, la esencia de iniciar el procesamiento de subprocesos asincrónicos después de bloquear la reactivación es la misma.

2. Java AIO es lo mismo que NIO, y los métodos de implementación subyacentes de cada plataforma también son diferentes. EPoll se usa en Linux, IOCP se usa en Windows y KQueue se usa en Mac OS. El principio es el mismo, todos requieren un subproceso de usuario para bloquear y esperar eventos de E/S y un grupo de subprocesos para procesar eventos de la cola.

3. La razón por la que Netty eliminó AIO es que AIO no es superior a NIO en términos de rendimiento. Aunque Linux también tiene un conjunto de implementaciones AIO nativas (similares a IOCP en Windows), Java AIO no se usa en Linux, pero se implementa con EPoll.

4. Java AIO no es compatible con UDP

5. El método de programación AIO es un poco complicado, como "devolución de llamada de muerte"

{{o.nombre}}
{{m.nombre}}

Supongo que te gusta

Origin my.oschina.net/u/5783135/blog/8570287
Recomendado
Clasificación