Какой ThreadPool в Java следует использовать?

существует огромное количество задач. Каждая задача принадлежит к одной группе. Требование состоит в том, что каждая группа задач должна выполняться последовательно, как и в одном потоке, а пропускная способность должна быть максимизирована в многоядерной (или многоядерной) среде. Примечание: также существует огромное количество групп, пропорционально количеству задач.

наивное решение использует ThreadPoolExecutor и синхронизирует (или блокирует). Тем не менее, потоки будут блокировать друг друга и пропускная способность не увеличена.

идея получше? Или существует сторонняя библиотека, удовлетворяющая этому требованию?

5 ответов


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

в качестве альтернативы рассмотрим цепочку задач группы. Следующий код иллюстрирует это:

public class MultiSerialExecutor {
    private final ExecutorService executor;

    public MultiSerialExecutor(int maxNumThreads) {
        executor = Executors.newFixedThreadPool(maxNumThreads);
    }

    public void addTaskSequence(List<Runnable> tasks) {
        executor.execute(new TaskChain(tasks));
    }

    private void shutdown() {
        executor.shutdown();
    }

    private class TaskChain implements Runnable {
        private List<Runnable> seq;
        private int ind;

        public TaskChain(List<Runnable> seq) {
            this.seq = seq;
        }

        @Override
        public void run() {
            seq.get(ind++).run(); //NOTE: No special error handling
            if (ind < seq.size())
                executor.execute(this);
        }       
    }

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

-- edit--

чтобы сделать это решение универсальным и полным, Вы можете принять решение об обработке ошибок (i.e продолжается ли цепочка, даже если возникает ошибка), а также было бы неплохо реализовать ExecutorService и делегировать все вызовы базовому исполнителю.


Я бы предложил использовать очереди задач:

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

быстрый поиск google предполагает, что Java api не имеет очередей задач / потоков сам по себе. Однако существует много учебных пособий по кодированию one. Все, не стесняйтесь перечислять хорошее учебники / реализации, если вы знаете некоторые:


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

class TaskAllocator {
    private final ConcurrentLinkedQueue<Queue<Runnable>> entireWork
         = childQueuePerTaskGroup();

    public Queue<Runnable> lockTaskGroup(){
        return entireWork.poll();
    }

    public void release(Queue<Runnable> taskGroup){
        entireWork.offer(taskGroup);
    }
 }

и

 class DoWork implmements Runnable {
     private final TaskAllocator allocator;

     public DoWork(TaskAllocator allocator){
         this.allocator = allocator;
     }

     pubic void run(){
        for(;;){
            Queue<Runnable> taskGroup = allocator.lockTaskGroup();
            if(task==null){
                //No more work
                return;
            }
            Runnable work = taskGroup.poll();
            if(work == null){
                //This group is done
                continue;
            }

            //Do work, but never forget to release the group to 
            // the allocator.
            try {
                work.run();
            } finally {
                allocator.release(taskGroup);
            }
        }//for
     }
 }

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

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

ConcurrentSkipListSet<MyQueue<Runnable>> sophisticatedQueue = 
    new ConcurrentSkipListSet(new SophisticatedComparator());

здесь SophisticatedComparator is

class SophisticatedComparator implements Comparator<MyQueue<Runnable>> {
    public int compare(MyQueue<Runnable> o1, MyQueue<Runnable> o2){
        int diff = o2.size() - o1.size();
        if(diff==0){
             //This is crucial. You must assign unique ids to your 
             //Subqueue and break the equality if they happen to have same size.
             //Otherwise your queues will disappear...
             return o1.id - o2.id;
        }
        return diff;
    }
 }

Actor также является еще одним решением для этого указанного типа проблем. Scala имеет актеров, а также Java, которые предоставляются AKKA.


у меня была проблема, похожая на вашу, и я использовал ExecutorCompletionService работает с Executor для выполнения коллекций задач. Вот выдержка из java.утиль.параллельный API, начиная с Java7:

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

void solve(Executor e, Collection<Callable<Result>> solvers)
        throws InterruptedException, ExecutionException {
    CompletionService<Result> ecs = new ExecutorCompletionService<Result>(e);
    for (Callable<Result> s : solvers)
        ecs.submit(s);
    int n = solvers.size();
    for (int i = 0; i < n; ++i) {
        Result r = ecs.take().get();
        if (r != null)
            use(r);
    }
}

таким образом, в вашем сценарии каждая задача будет одной Callable<Result>, и задачи будут сгруппированы в Collection<Callable<Result>>.

ссылка: http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ExecutorCompletionService.html