Как использовать несколько ядер в API неблокирующего ввода-вывода (NIO) JAVA?

JAVA NIO предоставляет API для написания TCP-сервера с использованием архитектуры NIO следующим образом.

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.text.ParseException;
import java.util.*;

public class NIOServer implements Runnable{
    private InetAddress addr;
    private int port;
    private Selector selector;

    public NIOServer(InetAddress addr, int port) throws IOException {
        this.addr = addr;
        this.port = port;
    }

    public void run(){
        try {
            startServer();
        }catch(IOException ex){
            System.out.println(ex.getMessage());
        }
    }

    private void startServer() throws IOException {

        this.selector = Selector.open();
        ServerSocketChannel serverChannel = serverSocketChannel.open();
        serverChannel.configureBlocking(false);
        InetSocketAddress listenAddr = new InetSocketAddress(this.addr, this.port);
        serverChannel.socket().bind(listenAddr);
        serverChannel.register(this.selector, SelectionKey.OP_ACCEPT);


        while (true) {

            this.selector.select();


            Iterator keys = this.selector.selectedKeys().iterator();
            while (keys.hasNext()) {
                SelectionKey key = (SelectionKey) keys.next();


                keys.remove();

                if (! key.isValid()) {
                    continue;
                }

                if (key.isAcceptable()) {
                    this.accept(key);
                }
                else if (key.isReadable()) {
                    this.read(key);
                }
                else if (key.isWritable()) {
                    this.write(key);
                }
            }
        }
    }
}

При этом используется один поток, который будет обрабатывать такие события, как чтение, запись и принятие.

По сравнению с архитектурой с блокировкой потока для каждого соединения это гораздо предпочтительнее из-за его неблокирующего характера, который вызывает минимальные промахи кеша, накладные расходы потоков, низкую миграцию процессора.

Однако эта архитектура использует только один поток. В многопроцессорной среде (например, 4-ядерный процессор) архитектура NIO тратит впустую другие ядра. Есть ли подход к проектированию, который я могу использовать для использования всех ядер с архитектурой NIO?

NIO2 (который основан на шаблоне proactor) является одним из таких вариантов. Но базовая архитектура сильно отличается от оригинальной NIO.


person Pasindu Tennage    schedule 25.06.2019    source источник


Ответы (1)


основная идея заключается в том, чтобы разделить задачу:

    ExecuterService workers = Executors.newFixedThreadPool(50);

    ....
    while (true) {

                this.selector.select();

                Iterator keys = this.selector.selectedKeys().iterator();
                while (keys.hasNext()) {
                    SelectionKey key = (SelectionKey) keys.next();

                    keys.remove();

                    if (! key.isValid()) {
                        continue;
                    }

                    if (key.isAcceptable()) {
                        this.accept(key);
                    }
                    else if (key.isReadable()) {
                        workers.execute(new ReadTaskHandler(key));
                    }
                    else if (key.isWritable()) {
                        workers.execute(new WriteTaskHandler(key));
                    }
                }
            }
class ReadTaskHandler implements Runnable {
    SelectionKey key;

    public ReadTaskHandler(SelectionKey key) {
        this.key = key;
    }

    @Override
    public void run() {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        SocketChannel channel = (SocketChannel) key.channel();

        int size = 0;
        try {
            while ((size = channel.read(buffer)) > 0) {
                System.out.println(new String(buffer.array()));
                buffer.flip();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

На самом деле существует множество моделей NIO. Например, мы также можем использовать несколько потоков для обработки задачи принятия (также называемую моделью множественных реакторов или моделью множественного цикла событий).

Кстати, Netty — это отличный управляемый событиями сетевой фреймворк приложений, упакованный в java NIO.

person Wang Kenneth    schedule 25.06.2019
comment
Согласно упомянутому вами NIO2 (который основан на шаблоне proactor), я хотел бы поделиться некоторыми дискуссиями о NIO и NIO2 (AIO): github.com/netty/netty/issues/2515 Нет улучшения производительности при использовании NIO2, особенно в LInux, потому что он использует ту же функцию ОС - epoll. - person Wang Kenneth; 26.06.2019
comment
Я использовал описанный выше подход, и он не значительно улучшает производительность, особенно когда требования к обработке запроса невелики. Однако, как вы упомянули, модель с несколькими реакторами или модель с несколькими циклами событий кажутся более перспективными. Не могли бы вы объяснить этот метод? - person Pasindu Tennage; 26.06.2019
comment
@PasinduTennage Во-первых, по сравнению с BIO, NIO не сильно улучшится, если будет недостаточно подключений/запросов (исходя из моего опыта, возможно, подключений ‹ 2000). Преимущество NIO в том, что вы можете использовать несколько потоков для обработки большого количества соединений, предполагая, что 2000 соединений будут стоить 2000 потоков в модели BIO. Поток — это своего рода дорогой ресурс. На самом деле, что касается моего подхода по сравнению с вашей версией, для демонстрации разницы в производительности требуется гораздо больше соединений (вероятно, до 10 000 ~ 50 000 подключений в секунду). - person Wang Kenneth; 26.06.2019
comment
@PasinduTennage Опять же, имейте в виду, что нить стоит дорого. Количество потоков должно быть гибким в соответствии с реальными соединениями и ядрами ЦП, а слишком большое количество потоков может плохо сказаться на производительности, поскольку переключение контекста потока требует много времени. а что касается модели с несколькими реакторами, то это для большей пропускной способности. Вы можете видеть мой подход как 1 мастер/босс со многими рабами/рабочими, поэтому несколько реакторов — это 1~3 мастера/босса со многими рабами/рабочими. несколько реакторов могут улучшить только тогда, когда узким местом производительности является прием соединений. - person Wang Kenneth; 26.06.2019
comment
Иллюстрация для mainReactors, subReactors с большим количеством воркеров: user-gold-cdn .xitu.io/2018/11/5/166e31ccfc6fd6b1?imageslim - person Wang Kenneth; 26.06.2019