ExecutorService.submit(‹callable›) занимает больше времени?

Я пытаюсь понять утилиты в пакете java.util.concurrent и узнал, что мы можем отправлять объекты callable в ExecutorService, который возвращает Future, который заполняется значением, возвращаемым callable, после успешного завершения задачи в методе call().

Я понимаю, что все вызываемые объекты выполняются одновременно с использованием нескольких потоков.

Когда я хотел посмотреть, насколько ExecutorService улучшает выполнение пакетных задач, я подумал о том, чтобы зафиксировать время.

Ниже приведен код, который я пытался выполнить:

package concurrency;


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;


public class ExecutorExample {

    private static Callable<String> callable = new Callable<String>() {

        @Override
        public String call() throws Exception {
            StringBuilder builder = new StringBuilder();
            for(int i=0; i<5; i++) {
                builder.append(i);
            }
            return builder.toString();
        }
    };

    public static void main(String [] args) {
        long start = System.currentTimeMillis();
        ExecutorService service = Executors.newFixedThreadPool(5);
        List<Future<String>> futures = new ArrayList<Future<String>>();
        for(int i=0; i<5; i++) {
            Future<String> value = service.submit(callable);
            futures.add(value);
        }
        for(Future<String> f : futures) {
            try {
                System.out.println(f.isDone() + " " + f.get());
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (ExecutionException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
        long end  = System.currentTimeMillis();
        System.out.println("Executer callable time - " + (end - start));
        service.shutdown();

        start = System.currentTimeMillis();
        for(int i=0; i<5; i++) {
            StringBuilder builder = new StringBuilder();
            for(int j=0; j<5; j++) {
                builder.append(j);
            }
            System.out.println(builder.toString());
        }
        end = System.currentTimeMillis();
        System.out.println("Normal time - " + (end - start));
    }

}

и вот вывод этого -

true 01234
true 01234
true 01234
true 01234
true 01234
Executer callable time - 5
01234
01234
01234
01234
01234
Normal time - 0

Пожалуйста, дайте мне знать, если я что-то упустил ИЛИ неправильно понял.

Заранее спасибо за ваше время и помощь в этой теме.


person sanbhat    schedule 14.03.2013    source источник
comment
Что вы делаете неправильно, так это то, что этот тест waaay слишком мал, чтобы использовать преимущества использования нескольких потоков для выполнения работы. Попробуйте то, что требует времени. Например, вычисление первых 10000 простых чисел или что-то в этом роде в каждом потоке... Кроме того, в то же время: вы не выполняете никаких прогревочных итераций: JVM может потребоваться выполнить некоторые действия при первом выполнении данного блока кода. , поэтому вы должны сделать несколько итераций, которые не учитываются в конечном результате...   -  person ppeterka    schedule 14.03.2013
comment
@ppeterka спасибо за ваш комментарий. Однако я попробовал Runnable вместо Callable (ExecutorService.submit(‹runnable›) с той же задачей. И я увидел улучшение времени выполнения всей задачи. Runnable заняло 0 миллисекунд разницы. Поэтому я подумал задать этот вопрос.   -  person sanbhat    schedule 14.03.2013
comment
Был ли тест с Runnable выполнен после теста с Callable в том же исполнении JVM? И как сравнить числа, если сделать 10000 итераций в цикле? Измерение времени в этом диапазоне в лучшем случае проблематично...   -  person ppeterka    schedule 14.03.2013
comment
Да, основной метод имел возможность вызова, выполнения и нормальное выполнение   -  person sanbhat    schedule 14.03.2013
comment
прочитайте вторую половину моего первого комментария: у вас должны быть прогревочные итерации, которые никак не измеряются. JVM необходимо выполнить некоторую инициализацию, которая занимает значительное время только при первом запуске определенной части кода. Скопируйте содержимое основного метода 2 раза друг за другом, чтобы понять, что я имею в виду.   -  person ppeterka    schedule 14.03.2013


Ответы (3)


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

person Andrey    schedule 14.03.2013

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

я предлагаю тебе

  • увеличить стоимость задачи до 1000 итераций.
  • убедитесь, что результат не отбрасывается в примере с одним потоком
  • запустите оба теста хотя бы на пару секунд, чтобы убедиться, что код прогрелся.
person Peter Lawrey    schedule 14.03.2013

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

import java.util.concurrent.*;
import java.util.concurrent.atomic.*;

public class FibonacciFork extends RecursiveTask<Long> {

private static final long serialVersionUID = 1L;

public FibonacciFork( long n) {
    super();
    this.n = n;
}

static ForkJoinPool fjp = new ForkJoinPool( Runtime.getRuntime().availableProcessors());

static long fibonacci0( long n) {
    if ( n < 2) {
        return n;
    }
    return fibonacci0( n - 1) + fibonacci0( n - 2);
}

static int  rekLimit = 8;

private static long stealCount;

long    n;

private long forkCount;

private static AtomicLong forks = new AtomicLong( 0);

public static void main( String[] args) {

    int n = 45;
    long    times[] = getSingleThreadNanos( n);
    System.out.println( "Single Thread Times complete");
    for ( int r = 2;  r <= n;  r++) {
        runWithRecursionLimit( r, n, times[ r]);
    }
}

private static long[] getSingleThreadNanos( int n) {
    final long times[] = new long[ n + 1];
    ExecutorService es = Executors.newFixedThreadPool( Math.max( 1, Runtime.getRuntime().availableProcessors() / 2));
    for ( int i = 2;  i <= n;  i++) {
        final int arg = i;
        Runnable runner = new Runnable() {
            @Override
            public void run() {
                long    start = System.nanoTime();
                final int minRuntime = 1000000000;
                long    runUntil = start + minRuntime;
                long    result = fibonacci0( arg);
                long    end = System.nanoTime();
                int         ntimes = Math.max( 1, ( int) ( minRuntime / ( end - start)));
                if ( ntimes > 1) {
                    start = System.nanoTime();
                    for ( int i = 0;  i < ntimes;  i++) {
                        result = fibonacci0( arg);
                    }
                    end = System.nanoTime();
                }
                times[ arg] = ( end - start) / ntimes;
            }
        };
        es.execute( runner);
    }
    es.shutdown();
    try {
        es.awaitTermination( 1, TimeUnit.HOURS);
    } catch ( InterruptedException e) {
        System.out.println( "Single Timeout");
    }
    return times;
}

private static void runWithRecursionLimit( int r, int arg, long singleThreadNanos) {
    rekLimit = r;
    long    start = System.currentTimeMillis();
    long    result = fibonacci( arg);
    long    end = System.currentTimeMillis();
    // Steals zählen
    long    currentSteals = fjp.getStealCount();
    long    newSteals = currentSteals - stealCount;
    stealCount = currentSteals;
    long    forksCount = forks.getAndSet( 0);
    System.out.println( "Fib(" + arg + ")=" + result + " in " + ( end-start) + "ms, recursion limit: " + r +
            " at " + ( singleThreadNanos / 1e6) + "ms, steals: " + newSteals + " forks " + forksCount);
}

static long fibonacci( final long arg) {
    FibonacciFork   task = new FibonacciFork( arg);
    long result = fjp.invoke( task);
    forks.set( task.forkCount);
    return result;
}

@Override
protected Long compute() {
    if ( n <= rekLimit) {
        return fibonacci0( n);
    }
    FibonacciFork   ff1 = new FibonacciFork( n-1);
    FibonacciFork   ff2 = new FibonacciFork( n-2);
    ff1.fork();
    long    r2 = ff2.compute();
    long    r1 = ff1.join();
    forkCount = ff2.forkCount + ff1.forkCount + 1;
    return r1 + r2;
}
}
person Ralf H    schedule 14.03.2013