Неблокирующий ввод-вывод против использования потоков (насколько плохо происходит переключение контекста)?

Мы много используем сокеты в программе, над которой я работаю, и иногда обрабатываем соединения от примерно до 100 машин одновременно. У нас есть комбинация неблокирующего I/O, используемого с таблицей состояний для управления ею и традиционными сокетами Java, которые используют потоки.

У нас довольно много проблем с неблокирующими сокетами, и мне лично нравится использовать потоки для более эффективного управления сокетами. Поэтому мой вопрос:

Сколько экономии достигается за счет использования неблокирующих сокетов в одном потоке? Насколько плохо происходит переключение контекста при использовании потоков и сколько параллельных подключений можно масштабировать для использования потоковой модели в Java?

Ответ 1

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

О цене переключения контекста - это скорее операция чипа. Рассмотрим простой тест ниже:

package com;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class AAA {

    private static final long DURATION = TimeUnit.NANOSECONDS.convert(30, TimeUnit.SECONDS);
    private static final int THREADS_NUMBER = 2;
    private static final ThreadLocal<AtomicLong> COUNTER = new ThreadLocal<AtomicLong>() {
        @Override
        protected AtomicLong initialValue() {
            return new AtomicLong();
        }
    };
    private static final ThreadLocal<AtomicLong> DUMMY_DATA = new ThreadLocal<AtomicLong>() {
        @Override
        protected AtomicLong initialValue() {
            return new AtomicLong();
        }
    };
    private static final AtomicLong DUMMY_COUNTER = new AtomicLong();
    private static final AtomicLong END_TIME = new AtomicLong(System.nanoTime() + DURATION);

    private static final List<ThreadLocal<CharSequence>> DUMMY_SOURCE = new ArrayList<ThreadLocal<CharSequence>>();
    static {
        for (int i = 0; i < 40; ++i) {
            DUMMY_SOURCE.add(new ThreadLocal<CharSequence>());
        }
    }

    private static final Set<Long> COUNTERS = new ConcurrentSkipListSet<Long>();

    public static void main(String[] args) throws Exception {
        final CountDownLatch startLatch = new CountDownLatch(THREADS_NUMBER);
        final CountDownLatch endLatch = new CountDownLatch(THREADS_NUMBER);

        for (int i = 0; i < THREADS_NUMBER; i++) {
            new Thread() {
                @Override
                public void run() {
                    initDummyData();
                    startLatch.countDown();
                    try {
                        startLatch.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    while (System.nanoTime() < END_TIME.get()) {
                        doJob();
                    }
                    COUNTERS.add(COUNTER.get().get());
                    DUMMY_COUNTER.addAndGet(DUMMY_DATA.get().get());
                    endLatch.countDown();
                }
            }.start();
        }
        startLatch.await();
        END_TIME.set(System.nanoTime() + DURATION);

        endLatch.await();
        printStatistics();
    }

    private static void initDummyData() {
        for (ThreadLocal<CharSequence> threadLocal : DUMMY_SOURCE) {
            threadLocal.set(getRandomString());
        }
    }

    private static CharSequence getRandomString() {
        StringBuilder result = new StringBuilder();
        Random random = new Random();
        for (int i = 0; i < 127; ++i) {
            result.append((char)random.nextInt(0xFF));
        }
        return result;
    }

    private static void doJob() {
        Random random = new Random();
        for (ThreadLocal<CharSequence> threadLocal : DUMMY_SOURCE) {
            for (int i = 0; i < threadLocal.get().length(); ++i) {
                DUMMY_DATA.get().addAndGet(threadLocal.get().charAt(i) << random.nextInt(31));
            }
        }
        COUNTER.get().incrementAndGet();
    }

    private static void printStatistics() {
        long total = 0L;
        for (Long counter : COUNTERS) {
            total += counter;
        }
        System.out.printf("Total iterations number: %d, dummy data: %d, distribution:%n", total, DUMMY_COUNTER.get());
        for (Long counter : COUNTERS) {
            System.out.printf("%f%%%n", counter * 100d / total);
        }
    }
}

Я сделал четыре теста для двух и десяти сценариев потока, и это показывает, что потеря производительности составляет около 2,5% (78626 итераций для двух потоков и 76754 для десяти потоков), системные ресурсы используются потоками примерно одинаково.

Также авторы 'java.util.concurrent' предполагают, что время переключения контекста составляет около 2000-4000 циклов процессора:

public class Exchanger<V> {
   ...
   private static final int NCPU = Runtime.getRuntime().availableProcessors();
   ....
   /**
    * The number of times to spin (doing nothing except polling a
    * memory location) before blocking or giving up while waiting to
    * be fulfilled.  Should be zero on uniprocessors.  On
    * multiprocessors, this value should be large enough so that two
    * threads exchanging items as fast as possible block only when
    * one of them is stalled (due to GC or preemption), but not much
    * longer, to avoid wasting CPU resources.  Seen differently, this
    * value is a little over half the number of cycles of an average
    * context switch time on most systems.  The value here is
    * approximately the average of those across a range of tested
    * systems.
    */
   private static final int SPINS = (NCPU == 1) ? 0 : 2000; 

Ответ 2

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

Прежде чем начать, сколько потоков вы говорите? И с каким типом оборудования вы используете свое программное обеспечение?

Ответ 3

Для 100 подключений вряд ли возникнет проблема с блокировкой ввода-вывода и использованием двух потоков для каждого соединения (один для чтения и записи). Это самая простая модель IMHO.

Однако вы можете обнаружить, что использование JMS - лучший способ управлять вашими подключениями. Если вы используете что-то вроде ActiveMQ, вы можете консолидировать все свои соединения.