Написание многопоточного итератора сопоставления в Java

У меня есть итератор с общим назначением: что-то вроде этого:

class Mapper<F, T> implements Iterator<T> {

  private Iterator<F> input;
  private Action<F, T> action;

  public Mapper(input, action) {...}

  public boolean hasNext() {
    return input.hasNext();
  }

  public T next() {
    return action.process(input.next());
  }
}

Теперь, учитывая, что action.process() может занять много времени, я хочу повысить производительность, используя несколько потоков для обработки элементов из входа параллельно. Я хочу выделить пул из N рабочих потоков и выделить элементы для этих потоков для обработки. Это должно происходить "за кулисами", поэтому клиентский код просто видит итератор. Код должен избегать использования входной или выходной последовательности в памяти.

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

У меня есть такая работа, но код кажется запутанным и ненадежным, и я не уверен, что он использует лучшие практики.

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

Ответ 1

Я бы использовал пул потоков для потоков и BlockingQueue для подачи из пула.

Это похоже на мои простые тестовые примеры.

interface Action<F, T> {

    public T process(F f);

}

class Mapper<F, T> implements Iterator<T> {

    protected final Iterator<F> input;
    protected final Action<F, T> action;

    public Mapper(Iterator<F> input, Action<F, T> action) {
        this.input = input;
        this.action = action;
    }

    @Override
    public boolean hasNext() {
        return input.hasNext();
    }

    @Override
    public T next() {
        return action.process(input.next());
    }
}

class ParallelMapper<F, T> extends Mapper<F, T> {

    // The pool.
    final ExecutorService pool;
    // The queue.
    final BlockingQueue<T> queue;
    // The next one to deliver.
    private T next = null;

    public ParallelMapper(Iterator<F> input, Action<F, T> action, int threads, int queueLength) {
        super(input, action);
        // Start my pool.
        pool = Executors.newFixedThreadPool(threads);
        // And the queue.
        queue = new ArrayBlockingQueue<>(queueLength);
    }

    class Worker implements Runnable {

        final F f;
        private T t;

        public Worker(F f) {
            this.f = f;
        }

        @Override
        public void run() {
            try {
                queue.put(action.process(f));
            } catch (InterruptedException ex) {
                // Not sure what you can do here.
            }
        }

    }

    @Override
    public boolean hasNext() {
        // All done if delivered it and the input is empty and the queue is empty and the threads are finished.
        while (next == null && (input.hasNext() || !queue.isEmpty() || !pool.isTerminated())) {
            // First look in the queue.
            next = queue.poll();
            if (next == null) {
                // Queue empty.
                if (input.hasNext()) {
                    // Start a new worker.
                    pool.execute(new Worker(input.next()));
                }
            } else {
                // Input exhausted - shut down the pool - unless we already have.
                if (!pool.isShutdown()) {
                    pool.shutdown();
                }
            }
        }
        return next != null;
    }

    @Override
    public T next() {
        T n = next;
        if (n != null) {
            // Delivered that one.
            next = null;
        } else {
            // Fails.
            throw new NoSuchElementException();
        }
        return n;
    }
}

public void test() {
    List<Integer> data = Arrays.asList(5, 4, 3, 2, 1, 0);
    System.out.println("Data");
    for (Integer i : Iterables.in(data)) {
        System.out.println(i);
    }
    Action<Integer, Integer> action = new Action<Integer, Integer>() {

        @Override
        public Integer process(Integer f) {
            try {
                // Wait that many seconds.
                Thread.sleep(1000L * f);
            } catch (InterruptedException ex) {
                // Just give up.
            }
            // Return it unchanged.
            return f;
        }

    };
    System.out.println("Processed");
    for (Integer i : Iterables.in(new Mapper<Integer, Integer>(data.iterator(), action))) {
        System.out.println(i);
    }
    System.out.println("Parallel Processed");
    for (Integer i : Iterables.in(new ParallelMapper<Integer, Integer>(data.iterator(), action, 2, 2))) {
        System.out.println(i);
    }

}

Примечание: Iterables.in(Iterator<T>) просто создает Iterable<T>, который инкапсулирует прошедший Iterator<T>.

Для вашего in-order one вы можете обработать Pair<Integer,F> и использовать PriorityQueue для вывода потока. Затем вы можете упорядочить их порядок.

Ответ 2

Я не думаю, что он может работать с параллельными потоками, потому что hasNext() может возвращать true, но к тому времени, когда поток вызывает next(), не может быть больше элементов. Лучше использовать только next(), который будет возвращать null, когда theres no more elements

Ответ 3

ОК, спасибо всем. Это то, что я сделал.

Сначала я переношу свою ItemMappingFunction в вызываемый:

private static class CallableAction<F extends Item, T extends Item> 
implements Callable<T> {
    private ItemMappingFunction<F, T> action;
    private F input;
    public CallableAction(ItemMappingFunction<F, T> action, F input) {
            this.action = action;
            this.input = input;
    }
    public T call() throws XPathException {
            return action.mapItem(input);
    }
}

Я описал свою проблему с точки зрения стандартного класса Iterator, но на самом деле я использую свой собственный интерфейс SequenceIterator, у которого есть единственный метод next(), который возвращает null в конце последовательности.

Я объявляю класс в терминах "обычного" итератора отображения следующим образом:

public class MultithreadedMapper<F extends Item, T extends Item> extends Mapper<F, T> {

    private ExecutorService service;
    private BlockingQueue<Future<T>> resultQueue = 
        new LinkedBlockingQueue<Future<T>>();

При инициализации я создаю службу и заправляю очередь:

public MultithreadedMapper(SequenceIterator base, ItemMappingFunction<F, T> action) throws XPathException {
        super(base, action);

        int maxThreads = Runtime.getRuntime().availableProcessors();
        maxThreads = maxThreads > 0 ? maxThreads : 1;
        service = Executors.newFixedThreadPool(maxThreads);

        // prime the queue
        int n = 0;
        while (n++ < maxThreads) {
            F item = (F) base.next();
            if (item == null) {
                return;
            }
            mapOneItem(item);
        }
    }

Где mapOneItem:

private void mapOneItem(F in) throws XPathException {
    Future<T> future = service.submit(new CallableAction(action, in));
    resultQueue.add(future);
}

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

    public T next() throws XPathException {
        F nextIn = (F)base.next();
        if (nextIn != null) {
            mapOneItem(nextIn);
        }
        try {
            Future<T> future = resultQueue.poll();
            if (future == null) {
                service.shutdown();
                return null;
            } else {
                return future.get();
            }
        } catch (InterruptedException e) {
            throw new XPathException(e);
        } catch (ExecutionException e) {
            if (e.getCause() instanceof XPathException) {
                throw (XPathException)e.getCause();
            }
            throw new XPathException(e);
        }
    }

Ответ 4

Для того, чтобы action.process вызывался параллельно, next() нужно было бы вызывать параллельно. Это не хорошая практика. Вместо этого вы можете использовать ExecutorCompletionService.

См. fooobar.com/questions/320719/...

К сожалению, я считаю, что это дает вам возможность сохранить порядок.

Ответ 5

Я бы рекомендовал посмотреть на структуру исполнителей JDK. Создавайте задачи (Runnables) для ваших действий. Выполните их параллельно, используя пул потоков, если это необходимо или последовательно, если нет. Дайте порядковые номера задач, если вам нужен заказ в конце. Но, как отмечено в других ответах, итератор не работает очень хорошо для вас, так как вызов next() обычно не выполняется параллельно. Так вам даже нужен итератор или просто для обработки задач?