Многопоточное выполнение, где сохраняется заказ готовых рабочих элементов

У меня есть поток единиц работы, позволяет называть их "Рабочие элементы", которые обрабатываются последовательно (на данный момент). Я бы хотел ускорить обработку, выполнив многопоточную работу.

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

Что-то вроде этого:

   |.|
   |.|
   |4|
   |3|
   |2|    <- incoming queue
   |1|
  / | \
 2  1  3  <- worker threads
  \ | /
   |3|
   |2|    <- outgoing queue
   |1|

Я хотел бы решить эту проблему на Java, желательно без служб Executor, Futures и т.д., но с основными concurrency методами вроде wait(), notify() и т.д.

Причина: мои рабочие элементы очень маленькие и мелкозернистые, они заканчивают обработку примерно в 0,2 миллисекунды каждый. Поэтому я боюсь использовать материал из java.util.concurrent. * Может привести к большим накладным расходам и замедлить мой код.

Примеры, которые я нашел до сих пор, сохраняют порядок во время обработки (что не имеет значения в моем случае) и не заботятся о порядке после обработки (что имеет решающее значение в моем случае).

Ответ 1

Если вы разрешите BlockingQueue, почему вы проигнорируете остальную часть concurrency utils в java? Вы можете использовать, например. Stream (если у вас есть java 1.8) для вышеперечисленного:

List<Type> data = ...;
List<Other> out = data.parallelStream()
    .map(t -> doSomeWork(t))
    .collect(Collectors.toList());

Поскольку вы начали с упорядоченного Collection (List) и собрали также в List, вы получите результаты в том же порядке, что и вход.

Ответ 2

Вот как я решил вашу проблему в предыдущем проекте (но с java.util.concurrent):

(1) Класс WorkItem выполняет фактическую работу/обработку:

public class WorkItem implements Callable<WorkItem> {
    Object content;
    public WorkItem(Object content) {
        super();
        this.content = content;
    }

    public WorkItem call() throws Exception {
        // getContent() + do your processing
        return this;
    }
}

(2) Этот класс помещает рабочие элементы в очередь и инициирует обработку:

public class Producer {
    ...
    public Producer() {
        super();
        workerQueue = new ArrayBlockingQueue<Future<WorkItem>>(THREADS_TO_USE);
        completionService = new ExecutorCompletionService<WorkItem>(Executors.newFixedThreadPool(THREADS_TO_USE));
        workerThread = new Thread(new Worker(workerQueue));
        workerThread.start();
    }

    public void send(Object o) throws Exception {
        WorkItem workItem = new WorkItem(o);
        Future<WorkItem> future = completionService.submit(workItem);
        workerQueue.put(future);
    }
}

(3) Как только обработка завершена, Рабочие элементы выгружаются здесь:

public class Worker implements Runnable {
    private ArrayBlockingQueue<Future<WorkItem>> workerQueue = null;

    public Worker(ArrayBlockingQueue<Future<WorkItem>> workerQueue) {
        super();
        this.workerQueue = workerQueue;
    }

    public void run() {
        while (true) {
            Future<WorkItem> fwi = workerQueue.take(); // deqeueue it
            fwi.get(); // wait for it till it has finished processing
        }
    }
}

(4) Вот как вы можете использовать материал в своем коде и представить новую работу:

public class MainApp {
    public static void main(String[] args) throws Exception {
        Producer p = new Producer();
        for (int i = 0; i < 10000; i++)
            p.send(i);
    }
}

Ответ 3

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

public class SequentialPushingProxy {

    static class OrderedJob implements Comparable<OrderedJob>{
        static AtomicInteger idSource = new AtomicInteger();
        int id;

        public OrderedJob() {
            id = idSource.incrementAndGet();
        }

        public int getId() {
            return id;
        }

        @Override
        public int compareTo(OrderedJob o) {
            return Integer.compare(id, o.getId());
        }
    }

    int lastId = OrderedJob.idSource.get();

    public Queue<OrderedJob> queue;

    public SequentialPushingProxy() {
        queue = new PriorityQueue<OrderedJob>();
    }

    public synchronized void pushResult(OrderedJob job) {
        queue.add(job);
    }

    List<OrderedJob> jobsToReturn = new ArrayList<OrderedJob>();
    public synchronized List<OrderedJob> getFinishedJobs() {
        while (queue.peek() != null) {
            // only one consumer at a time, will be safe
            if (queue.peek().getId() == lastId+1) {
                jobsToReturn.add(queue.poll());
                lastId++;
            } else {
                break;
            }
        }
        if (jobsToReturn.size() != 0) {
            List<OrderedJob> toRet = jobsToReturn;
            jobsToReturn = new ArrayList<OrderedJob>();
            return toRet;
        }
        return Collections.emptyList();
    }

    public static void main(String[] args) {
        final SequentialPushingProxy proxy = new SequentialPushingProxy();

        int numProducerThreads = 5;

        for (int i=0; i<numProducerThreads; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while(true) {
                        proxy.pushResult(new OrderedJob());
                    }
                }
            }).start();
        }


        int numConsumerThreads = 1;

        for (int i=0; i<numConsumerThreads; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while(true) {
                        List<OrderedJob> ret = proxy.getFinishedJobs();
                        System.out.println("got "+ret.size()+" finished jobs");
                        try {
                            Thread.sleep(200);
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }
                }
            }).start();
        }


        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        System.exit(0);
    }

}

Этот код можно легко улучшить до

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

Ответ 4

Насос всех ваших фьючерсов через BlockingQueue. Здесь нужен весь код:

public class SequentialProcessor implements Consumer<Task> {
    private final ExecutorService executor = Executors.newCachedThreadPool();
    private final BlockingDeque<Future<Result>> queue = new LinkedBlockingDeque<>();

    public SequentialProcessor(Consumer<Result> listener) {
        new Thread(() -> {
            while (true) {
                try {
                    listener.accept(queue.take().get());
                } catch (InterruptedException | ExecutionException e) {
                    // handle the exception however you want, perhaps just logging it
                }
            }
        }).start();
    }

    public void accept(Task task) {
        queue.add(executor.submit(callableFromTask(task)));
    }

    private Callable<Result> callableFromTask(Task task) {
        return <how to create a Result from a Task>; // implement this however
    }
}

Затем для использования создайте SequentialProcessor (один раз):

SequentialProcessor processor = new SequentialProcessor(whatToDoWithResults);

и выполните следующие задачи:

Stream<Task> tasks; // given this

tasks.forEach(processor); // simply this

Я создал метод callableFromTask() для иллюстрации, но вы можете обойтись без него, если получить Result из Task просто, используя вместо этого ссылку лямбда или ссылку на метод.

Например, если Task имеет метод getResult(), выполните следующее:

queue.add(executor.submit(task::getResult));

или если вам нужно выражение (лямбда):

queue.add(executor.submit(() -> task.getValue() + "foo")); // or whatever

Ответ 5

У вас может быть 3 входных и 3 очереди вывода - один из каждого типа для каждого рабочего потока.

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

Все очереди должны блокироваться.

Ответ 6

Реактивное программирование может помочь. Во время моего краткого опыта работы с RxJava я нашел его интуитивно понятным и легким в работе, чем основные функции языка, такие как Future и т.д. Ваш пробег может отличаться. Вот несколько полезных отправных точек https://www.youtube.com/watch?v=_t06LRX0DV0

Приложенный пример также показывает, как это можно сделать. В приведенном ниже примере у нас есть пакет, который необходимо обработать. Они берутся через простую трансформацию и объединяются в один список. Результат, добавленный к этому сообщению, показывает, что пакеты принимаются и преобразуются в разные моменты времени, но в конце они выводятся в том порядке, в котором они были получены.

import static java.time.Instant.now;
import static rx.schedulers.Schedulers.io;

import java.time.Instant;
import java.util.List;
import java.util.Random;

import rx.Observable;
import rx.Subscriber;

public class RxApp {

  public static void main(String... args) throws InterruptedException {

    List<ProcessedPacket> processedPackets = Observable.range(0, 10) //
        .flatMap(i -> {
          return getPacket(i).subscribeOn(io());
        }) //
        .map(Packet::transform) //
        .toSortedList() //
        .toBlocking() //
        .single();

    System.out.println("===== RESULTS =====");
    processedPackets.stream().forEach(System.out::println);
  }

  static Observable<Packet> getPacket(Integer i) {
    return Observable.create((Subscriber<? super Packet> s) -> {
      // simulate latency
      try {
        Thread.sleep(new Random().nextInt(5000));
      } catch (Exception e) {
        e.printStackTrace();
      }
      System.out.println("packet requested for " + i);
      s.onNext(new Packet(i.toString(), now()));
      s.onCompleted();
    });
  }

}


class Packet {
  String aString;
  Instant createdOn;

  public Packet(String aString, Instant time) {
    this.aString = aString;
    this.createdOn = time;
  }

  public ProcessedPacket transform() {
    System.out.println("                          Packet being transformed " + aString);
    try {
      Thread.sleep(new Random().nextInt(5000));
    } catch (Exception e) {
      e.printStackTrace();
    }
    ProcessedPacket newPacket = new ProcessedPacket(this, now());
    return newPacket;
  }

  @Override
  public String toString() {
    return "Packet [aString=" + aString + ", createdOn=" + createdOn + "]";
  }
}


class ProcessedPacket implements Comparable<ProcessedPacket> {
  Packet p;
  Instant processedOn;

  public ProcessedPacket(Packet p, Instant now) {
    this.p = p;
    this.processedOn = now;
  }

  @Override
  public int compareTo(ProcessedPacket o) {
    return p.createdOn.compareTo(o.p.createdOn);
  }

  @Override
  public String toString() {
    return "ProcessedPacket [p=" + p + ", processedOn=" + processedOn + "]";
  }

}

Деконструкция

Observable.range(0, 10) //
    .flatMap(i -> {
      return getPacket(i).subscribeOn(io());
    }) // source the input as observables on multiple threads


    .map(Packet::transform) // processing the input data 

    .toSortedList() // sorting to sequence the processed inputs; 
    .toBlocking() //
    .single();

В одном конкретном запуске Пакеты были получены в заказе 2,6,0,1,8,7,5,9,4,3 и обработаны в порядке 2,6,0,1,3,4,5, 7,8,9 для разных потоков

packet requested for 2
                          Packet being transformed 2
packet requested for 6
                          Packet being transformed 6
packet requested for 0
packet requested for 1
                          Packet being transformed 0
packet requested for 8
packet requested for 7
packet requested for 5
packet requested for 9
                          Packet being transformed 1
packet requested for 4
packet requested for 3
                          Packet being transformed 3
                          Packet being transformed 4
                          Packet being transformed 5
                          Packet being transformed 7
                          Packet being transformed 8
                          Packet being transformed 9
===== RESULTS =====
ProcessedPacket [p=Packet [aString=2, createdOn=2016-04-14T13:48:52.060Z], processedOn=2016-04-14T13:48:53.247Z]
ProcessedPacket [p=Packet [aString=6, createdOn=2016-04-14T13:48:52.130Z], processedOn=2016-04-14T13:48:54.208Z]
ProcessedPacket [p=Packet [aString=0, createdOn=2016-04-14T13:48:53.989Z], processedOn=2016-04-14T13:48:55.786Z]
ProcessedPacket [p=Packet [aString=1, createdOn=2016-04-14T13:48:54.109Z], processedOn=2016-04-14T13:48:57.877Z]
ProcessedPacket [p=Packet [aString=8, createdOn=2016-04-14T13:48:54.418Z], processedOn=2016-04-14T13:49:14.108Z]
ProcessedPacket [p=Packet [aString=7, createdOn=2016-04-14T13:48:54.600Z], processedOn=2016-04-14T13:49:11.338Z]
ProcessedPacket [p=Packet [aString=5, createdOn=2016-04-14T13:48:54.705Z], processedOn=2016-04-14T13:49:06.711Z]
ProcessedPacket [p=Packet [aString=9, createdOn=2016-04-14T13:48:55.227Z], processedOn=2016-04-14T13:49:16.927Z]
ProcessedPacket [p=Packet [aString=4, createdOn=2016-04-14T13:48:56.381Z], processedOn=2016-04-14T13:49:02.161Z]
ProcessedPacket [p=Packet [aString=3, createdOn=2016-04-14T13:48:56.566Z], processedOn=2016-04-14T13:49:00.557Z]

Ответ 7

Вы можете запустить поток DoTask для каждого WorkItem. Этот поток обрабатывает работу. Когда работа будет выполнена, вы попытаетесь опубликовать элемент, синхронизированный на управляющем объекте, в котором вы проверяете, имеет ли он правильный идентификатор и ждет, если нет.

Пост-реализация может быть примерно такой:

synchronized(controllingObject) {
try {
while(workItem.id != nextId) controllingObject.wait();
} catch (Exception e) {}
//Post the workItem
nextId++;
object.notifyAll();
}

Ответ 8

Я думаю, что вам нужна дополнительная очередь для хранения входящего заказа. IncomingOrderQueue.

Когда вы потребляете объекты, которые вы помещаете в какое-либо хранилище, например карту, а затем из другого потока, который потребляет из IncomingOrderQueue, вы выбираете идентификаторы (хэши) объектов, а затем вы собираете их из этой HashMap.

Это решение может быть легко реализовано без выполнения службы.

Ответ 9

Preprocess: добавьте значение заказа каждому элементу, подготовьте массив, если он не выделен.

Вход: очередь (параллельная выборка с порядковыми значениями 1,2,3,4, но не имеет значения, какой протекторы получают, какой образец)

Выход: массив (запись в индексированные элементы с использованием точки синхронизации для ожидания всех потоков в конце не требует проверок конфликтов, поскольку она записывает разные позиции для каждого потока)

Postprocess: преобразовать массив в очередь.

Требуется n element-array для n-потоков. Или несколько кратных n, чтобы делать постобработку только один раз.