Veja a essência do Java AIO através do fenômeno|Dewu Technology

1. Introdução

Existem muitos artigos sobre as diferenças e princípios de Java BIO, NIO e AIO, mas eles são discutidos principalmente entre BIO e NIO, enquanto há muito poucos artigos sobre AIO, e muitos deles são apenas introduções. os conceitos e exemplos de código.

Ao aprender sobre AIO, os seguintes fenômenos foram notados:

1. Java 7 foi lançado em 2011, que adicionou um modelo de programação chamado AIO chamado IO assíncrono, mas quase 12 anos se passaram e o middleware de estrutura de desenvolvimento usual ainda é dominado por NIO, como a estrutura de rede Netty, Mina, contêiner da Web Tomcat, Ressaca.

2. Java AIO também é chamado de NIO 2.0. Também é baseado em NIO?

3. Netty abandonou o suporte da AIO. https://github.com/netty/netty/issues/2515

4. AIO parece ter apenas resolvido o problema e liberado uma solidão.
Esses fenômenos irão inevitavelmente confundir muitas pessoas, então quando decidi escrever este artigo, não queria simplesmente repetir o conceito de AIO, mas como analisar, pensar e entender a essência do Java AIO através do fenômeno.

2. O que é assíncrono

2.1 Assincronia como a conhecemos

O A de AIO significa Assíncrono.Antes de entender o princípio de AIO, vamos esclarecer que tipo de conceito é "assíncrono".
Falando em programação assíncrona, ela ainda é relativamente comum no desenvolvimento normal, como os exemplos de código a seguir:

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

Seja anotado com @Async ou enviando tarefas para o pool de threads, todos terminam com o mesmo resultado, que é entregar a tarefa a ser executada para outro thread para execução.
Neste momento, pode-se considerar aproximadamente que o chamado "assíncrono" é multiencadeado e executa tarefas.

2.2 Java BIO e NIO são síncronos ou assíncronos?

Se Java BIO e NIO são síncronos ou assíncronos, primeiro fazemos programação assíncrona de acordo com a ideia de assincronia.

2.2.1 Exemplo BIO

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

Quando BIO read(), embora o thread esteja bloqueado, ao receber dados, um thread pode ser iniciado de forma assíncrona para processar.

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

Da mesma forma, embora NIO read() não bloqueie, ele pode bloquear a espera de dados através de select().Quando há dados para ler, ele inicia uma thread assincronamente para ler e processar dados.

2.2.3 Desvios de Entendimento

Neste momento, juramos que se o BIO e o NIO do Java são assíncronos ou síncronos, depende do seu humor. Se você estiver feliz em dar a ele um multi-thread, é assíncrono.

Mas se for esse o caso, depois de ler muitos artigos de blog, fica basicamente esclarecido que BIO e NIO são sincronizados.

Então, onde está o problema?O que causou o desvio em nossa compreensão?

Esse é o problema do quadro de referência. Ao estudar física antes, se os passageiros do ônibus estão em movimento ou parados, é necessário um quadro de referência. Se o solo for usado como referência, ele está em movimento e o ônibus é usado como uma referência, ele é estacionário.

O mesmo vale para o Java IO. É necessário um sistema de referência para definir se ele é síncrono ou assíncrono. Como estamos discutindo qual é o modo de IO, é necessário entender as operações de leitura e gravação do IO, enquanto outros iniciam outro. Os encadeamentos para processar dados já estão fora do escopo da leitura e gravação de IO e não devem ser envolvidos.

2.2.4 Tentando definir assíncrono

Portanto, tomando como referência o evento das operações de leitura e gravação de IO, primeiro tentamos definir a thread que inicia a leitura e gravação de IO (a thread que chama read e write) e a thread que realmente opera a leitura e gravação de IO. eles são o mesmo thread, então chame-o de síncrono, caso contrário, assíncrono .

  • Obviamente, BIO só pode ser síncrono. Chamar in.read() bloqueia o thread atual. Quando os dados são retornados, o thread original recebe os dados.

  • E NIO também é chamado de sincronização, e o motivo é o mesmo: ao chamar channel.read(), embora o thread não bloqueie, ainda é o thread atual que lê os dados.

De acordo com essa ideia, o AIO deve ser o thread que inicia a leitura e a gravação do IO, e o thread que realmente recebe os dados pode não ser o mesmo thread. É este o
caso? Vamos iniciar o código Java AIO agora.

2.3 Exemplo de programa Java AIO

2.3.1 Programa do servidor 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 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 Conclusão da conjectura de definição assíncrona

Execute os programas de servidor e cliente separadamente

640.png

No resultado da execução do servidor,

O thread principal inicia uma chamada para serverChannel.accept e adiciona um CompletionHandler para monitorar o retorno de chamada.Quando um cliente se conecta, o thread Thread-5 executa o método de retorno de chamada concluído de accept.

Imediatamente depois, Thread-5 iniciou a chamada clientChannel.read e adicionou um CompletionHandler para monitorar o retorno de chamada.Ao receber dados, Thread-1 executou o método de retorno de chamada concluído de leitura.

Esta conclusão é consistente com a conjectura assíncrona acima. A thread que inicia a operação IO (como aceitar, ler, escrever) não é a mesma que a thread que finalmente conclui a operação. Chamamos esse modo IO de AIO .

É claro que definir AIO dessa forma é apenas para nosso entendimento.Na prática, a definição de IO assíncrona pode ser mais abstrata.

3. O exemplo de AIO leva a perguntas de reflexão

1. Quem criou o thread executando o método Completed() e quando ele foi criado?

2. Como implementar o monitoramento de eventos de registro AIO e callback de execução?

3. Qual é a essência do monitoramento de retorno de chamada?

3.1 Questão 1: Quem criou a thread que executa o método complete(), e quando ela foi criada

Geralmente tal problema precisa ser entendido desde a entrada do programa, mas está relacionado com a thread, de fato, é possível localizar como a thread roda a partir do estado running da pilha de threads.

Execute apenas o programa do servidor AIO, o cliente não executa, imprima a pilha de threads (Nota: o programa é executado na plataforma Linux e outras plataformas são ligeiramente diferentes)

6401.png

Analise a pilha de threads e descubra que o programa inicia tantos threads

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

2. Linha Linha-1, Linha-2. . . Thread-n (n é o mesmo que o número de núcleos da CPU) pega () tarefas da fila de bloqueio e bloqueia aguardando o retorno de uma tarefa.

Neste ponto, a próxima conclusão pode ser tirada provisoriamente:

Depois que o programa do servidor AIO é iniciado, esses encadeamentos são criados e todos os encadeamentos ficam em um estado de espera bloqueado.

Além disso, descobri que a execução desses threads está relacionada ao Epoll. Quando se trata do Epoll, temos a impressão de que o Java NIO é implementado com o Epoll na parte inferior da plataforma Linux. O Java AIO também é implementado com o Epoll ? Para confirmar esta conclusão, discutimos a partir da próxima pergunta

3.2 Pergunta 2: Como implementar o monitoramento de eventos de registro AIO e callback de execução

Com esse problema em mente, quando li e analisei o código-fonte, descobri que o código-fonte é muito longo e a análise do código-fonte é um processo chato, que pode facilmente afastar os leitores.

Para a compreensão de processos longos e códigos logicamente complexos, podemos entender seus vários contextos e descobrir quais processos centrais.

Pegue o listener de registro lido como um exemplo clientChannel.read(…), seu principal processo principal é:

1. Registrar evento -> 2. Ouvir evento -> 3. Processar evento

3.2.1 1. Evento de inscrição

6402.png

O evento de registro chama a função EPoll.ctl(…) e o último parâmetro desta função é usado para especificar se é único ou permanente. Os eventos de código acima | EPOLLONSHOT significam literalmente que é único.

3.2.2 2. Monitorar eventos

6408.png

3.2.3 3. Tratamento de eventos

6409.png

64010.png

64011.png

3.2.4 Resumo dos principais processos

64012.png

Depois de analisar o fluxo de código acima, você descobrirá que os três eventos que devem ser experimentados para cada leitura e gravação de IO são únicos, ou seja, depois que o evento é processado, esse processo termina. Se você quiser continuar no próximo IO Para ler e escrever, é preciso começar tudo de novo. Dessa forma, haverá o chamado retorno de chamada da morte (o próximo método de retorno de chamada é adicionado ao método de retorno de chamada), o que aumenta muito a complexidade da programação.

3.3 Pergunta 3: Qual é a essência do monitoramento de callbacks?

Deixe-me falar sobre a conclusão primeiro. A essência do chamado retorno de chamada de monitoramento é o thread do modo de usuário, que chama a função do modo kernel (falando com precisão, API, como read, write, epollWait). não retornado, o thread do usuário está bloqueado. Quando a função retorna, o thread bloqueado é ativado e a chamada função de retorno de chamada é executada .

Para entender essa conclusão, devemos primeiro introduzir vários conceitos

3.3.1 Chamadas de sistema e chamadas de função

chamada de função:

Encontre uma função e execute comandos relacionados na função

Chamada do sistema:

O sistema operacional fornece uma interface de programação para os aplicativos do usuário, a chamada API.

Processo de execução da chamada do sistema:

1. Passe os parâmetros de chamada do sistema

2. Execute instruções interceptadas, alterne do modo de usuário para o modo principal, porque as chamadas do sistema geralmente precisam ser executadas no modo principal

3. Execute o programa de chamada do sistema

4. Retorne ao estado do usuário

3.3.2 Comunicação entre o modo usuário e o modo kernel

Modo de usuário -> Modo de kernel, apenas por meio de chamadas do sistema.

Modo kernel -> modo usuário, o modo kernel não sabe quais funções o programa do modo usuário possui, quais são os parâmetros e onde está o endereço. Portanto, é impossível para o kernel chamar funções no modo usuário, mas apenas enviando sinais.Por exemplo, o comando kill para fechar o programa é permitir que o programa do usuário saia normalmente enviando sinais.

Como é impossível para o estado do kernel chamar funções ativamente no estado do usuário, por que há um retorno de chamada?Pode-se dizer apenas que esse chamado retorno de chamada é na verdade um estado de usuário autodirigido e executado. Ele não apenas monitora, mas também executa a função de retorno de chamada.

3.3.3 Verifique a conclusão com exemplos práticos

Para verificar se essa conclusão é convincente, por exemplo, o IntelliJ IDEA, que geralmente é usado para desenvolver e escrever código, escuta eventos de mouse e teclado e manipula eventos.

De acordo com a convenção, primeiro imprima a pilha de encadeamentos e você descobrirá que o encadeamento "AWT-XAWT" é responsável por monitorar eventos como mouse e teclado, e o encadeamento "AWT-EventQueue" é responsável pelo processamento de eventos.

64013.png

Localizando o código específico, você pode ver que "AWT-XAWT" está fazendo um loop while, chamando a função waitForEvents para aguardar o retorno do evento. Se não houver evento, o thread foi bloqueado lá.

64014.png

4. Qual é a essência do Java AIO?

1. Como o modo kernel não pode chamar diretamente as funções do modo usuário, a essência do Java AIO é implementar a assincronia apenas no modo usuário. Não alcança a assincronia no sentido ideal.

assíncrono ideal

O que é assincronia no sentido ideal? Aqui está um exemplo de compras online

Duas funções, consumidor A e entregador B

  • Quando A estiver fazendo compras online, preencha o endereço residencial para pagar e enviar o pedido, o que equivale a registrar o evento de monitoramento

  • O comerciante entrega as mercadorias e B entrega o item na porta de A, o que equivale a um retorno de chamada.

Depois que A faz o pedido online, ele não precisa se preocupar com o processo de entrega subsequente e pode continuar fazendo outras coisas. B não se importa se A está em casa ou não ao entregar as mercadorias. De qualquer forma, basta jogar as mercadorias na porta da casa. As duas pessoas não dependem uma da outra e não interferem uma na outra .

Supondo que as compras de A sejam feitas no modo de usuário e a entrega expressa de B seja feita no modo kernel, esse tipo de modo de operação do programa é ideal demais e não pode ser realizado na prática.

Assincronia na realidade

A mora em uma área residencial de alto padrão e não pode entrar à vontade, e o entregador só pode ser entregue no portão da área residencial.

A comprou um produto relativamente pesado, como uma TV, porque A estava indo trabalhar e não estava em casa, então pediu a um amigo C que ajudasse a levar a TV para sua casa.
Antes de sair para o trabalho, A cumprimenta o segurança D na porta, dizendo que hoje será entregue uma TV, quando for entregue na porta da comunidade, por favor, ligue para C e peça para ele vir buscá-la.

  • Nesse ponto, A faz um pedido e cumprimenta D, o que equivale a registrar um evento. No AIO é o evento de registro EPoll.ctl(...).

  • O segurança agachado na porta equivale a ouvir o evento. Em AIO, é a thread Thread-0. Faça EPoll.wait(…)

  • O mensageiro entregou a TV na porta, o que equivale à chegada de um evento IO.

  • O segurança avisa a C que a TV chegou, e C vem mover a TV, o que equivale a lidar com o incidente.

No AIO, Thread-0 envia tarefas para a fila de tarefas.

Thread-1 ~n para buscar dados e executar o método de retorno de chamada.

Durante todo o processo, o segurança D teve que ficar agachado o tempo todo, não podendo sair nem um centímetro, caso contrário a TV seria roubada quando fosse entregue na porta.

O amigo C também tem que ficar na casa de A. Ele é confiado por alguém, mas a pessoa não está lá quando as coisas chegam, isso é um pouco desonesto.

Portanto, a assincronia real e a assincronia ideal são independentes uma da outra e não interferem uma na outra, esses dois pontos são contrários um ao outro . O papel de segurança é o maior, e este é o momento de destaque de sua vida.

Registrando eventos, ouvindo eventos, processando eventos e habilitando multi-threading no processo assíncrono, os iniciadores desses processos são todos manipulados pelo modo de usuário, portanto, o Java AIO implementa apenas a assincronia no modo de usuário, que é bloqueado primeiro com o BIO e NIO , a essência de iniciar o processamento de encadeamento assíncrono após o bloqueio da ativação é a mesma.

2. Java AIO é o mesmo que NIO, e os métodos de implementação subjacentes de cada plataforma também são diferentes. EPoll é usado no Linux, IOCP é usado no Windows e KQueue é usado no Mac OS. O princípio é o mesmo, todos requerem um thread de usuário para bloquear e aguardar eventos de IO e um pool de threads para processar eventos da fila.

3. A razão pela qual Netty removeu o AIO é que o AIO não é superior ao NIO em termos de desempenho. Embora o Linux também tenha um conjunto de implementações AIO nativas (semelhantes ao IOCP no Windows), o Java AIO não é usado no Linux, mas é implementado com EPoll.

4. Java AIO não suporta UDP

5. O método de programação AIO é um pouco complicado, como "death callback"

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

Acho que você gosta

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