Потоковая сортировка работает медленнее, чем безпоточная сортировка

Я пытаюсь отсортировать файл с помощью потоков. Вот Sort.java:

Эта функция сортирует с помощью потоков

public static String[] threadedSort(File[] files) throws IOException {
      String sortedData[] = new String[0]; 
      int counter = 0; 
      boolean allThreadsTerminated = false;
      SortingThread[] threadList = new SortingThread[files.length];
      for (File file : files) {
          String[] data = getData(file);
          threadList[counter] = new SortingThread(data);
          threadList[counter].start();
          counter++;
      }
      while(!allThreadsTerminated) {
          allThreadsTerminated = true;
          for(counter=0; counter<files.length; counter++) {
              if(threadList[counter].getState() != Thread.State.TERMINATED) {
                  allThreadsTerminated = false;               
              }           
          }
      }
      for(counter=0; counter<files.length; counter++) {
          sortedData = MergeSort.merge(sortedData, threadList[counter].data);
      }
      return sortedData;
 }

Эта функция нормально сортирует

  public static String[] sort(File[] files) throws IOException {
    String[] sortedData = new String[0];
    for (File file : files) {
      String[] data = getData(file);
      data = MergeSort.mergeSort(data);
      sortedData = MergeSort.merge(sortedData, data);
    }
    return sortedData;
  }

Теперь, когда я сортирую обоими способами, обычная сортировка выполняется быстрее, чем многопоточная версия. Что может быть причиной этого? Я что-то пропустил?

Мой SortingThread выглядит примерно так:

public class SortingThread extends Thread {
    String[] data;
    SortingThread(String[] data) {
        this.data = data;
    }
    public void run() {
         data = MergeSort.mergeSort(data);        
    }  
}

Когда я анализирую свою многопоточную реализацию, сравнивая ее производительность с исходной беспоточной реализацией, я нахожу вторую быстрее. Что может быть причиной такого поведения? Если мы говорим об относительном улучшении производительности, мы ожидаем, что многопоточная реализация будет быстрее, если я не ошибаюсь.

РЕДАКТИРОВАТЬ: предположим, что у меня правильно работает MergeSort. Но бесполезно публиковать его код здесь. Также функция getData() предназначена только для ввода данных из файла. Я думаю, что проблема заключается в том, что я беру весь файл в массив. Я думаю, что я должен предоставить разные строки для разных потоков:

private static String[] getData(File file) throws IOException {
    ArrayList<String> data = new ArrayList<String>();
    BufferedReader in = new BufferedReader(new FileReader(file));
    while (true) {
      String line = in.readLine();
      if (line == null) {
        break;
      }
      else {
        data.add(line);
      }
    }


    in.close();
    return data.toArray(new String[0]);
  }

person ms8    schedule 07.06.2015    source источник
comment
Каковы ваши временные данные? Насколько это быстрее? Или, говоря словами Art of Noise: насколько быстро быстро? Кажется, вы сортируете содержимое файла. Доступ к файловой системе может быть узким местом. Создание потоков — тяжелый процесс, хотя, возможно, он и не дает никакой пользы.   -  person Roger Gustavsson    schedule 07.06.2015
comment
Если вам нужно выполнить операцию и объединить результат в конце, ForkJoinPool, вероятно, будет лучшим выбором.   -  person    schedule 07.06.2015
comment
@RogerGustavsson Sort.sort потребовалось 1,129517647 секунд, чтобы прочитать и отсортировать данные. Sort.threadedSort потребовалось 3,171421661 секунды для чтения и сортировки данных.   -  person ms8    schedule 07.06.2015
comment
@RogerGustavsson Итак, в этом случае ожидается, что потоки будут медленнее?   -  person ms8    schedule 07.06.2015
comment
@xTrollxDudex Как это сделать в моем приведенном выше коде? Пожалуйста помоги   -  person ms8    schedule 07.06.2015
comment
@python_slayer На самом деле я не знаю. Я подумал, что в посте могут быть полезны некоторые данные о времени, поскольку просто сказать, что что-то быстрее или медленнее, не всегда много значит. И мое предположение о том, что файловая система является узким местом, я думаю, стоит принять во внимание.   -  person Roger Gustavsson    schedule 07.06.2015
comment
@RogerGustavsson Какая может быть лучшая стратегия ввода-вывода для повышения производительности?   -  person ms8    schedule 07.06.2015


Ответы (4)


Прежде всего, как вы измеряете прошедшее время? Вы выполняете оба теста в одной программе? Если это так, имейте в виду, что сортировка слиянием, вероятно, подвергнется компиляции Hotspot во время выполнения первого теста. Я предлагаю вам запускать каждый метод дважды, измеряя время при втором запуске.

person Roberto Attias    schedule 07.06.2015
comment
Ты прав !!!! Если я сначала вызову обычную сортировку pastebin.com/j7nLAKkz, ее результаты будут отличаться от результатов, полученных при вызове сначала потоковой сортировки pastebin.com/6BzCmmxn. Что может быть причиной этого? Пожалуйста помоги - person ms8; 07.06.2015
comment
Другая причина заключается в том, что файлы кэшируются, когда вы запускаете первую сортировку, поэтому вторая сортировка читает файлы быстрее, и это может быть важнее, чем все потоки, которые вы делаете :) - person Michael Entin; 07.06.2015
comment
@Michael Майкл Как тогда удалить его из кеша? чтобы оба исполнения были независимыми - person ms8; 07.06.2015
comment
Как я уже писал, запускайте каждый тест дважды и измеряйте время во второй раз. - person Roberto Attias; 07.06.2015
comment
Мне нравится чередовать код, который я хочу сравнить несколько раз - первый вариант, второй вариант, снова первый, второй снова, первый снова, второй снова. Посмотрите все 6 раз, чтобы понять, как казни влияют друг на друга. - person Michael Entin; 07.06.2015
comment
@RobertoAttias Вы были правы !! Можете ли вы объяснить, почему это произошло именно так? - person ms8; 07.06.2015
comment
@Michael Если проблема была в кешировании, то второй вызов должен быть быстрее !! - person ms8; 07.06.2015

Сколько процессоров/ядер у вас есть? Одна из проблем с этим кодом заключается в том, что основной поток тратит процессорное время на цикл while(!allThreadsTerminated), активно проверяя состояние потока. Если у вас один ЦП - вы тратите его впустую, вместо того, чтобы заниматься фактической сортировкой.

Замените цикл while на:

 for(counter=0; counter<files.length; counter++) {
        threadList[counter].join();
 }
person Michael Entin    schedule 07.06.2015
comment
Вы имеете в виду заменить цикл while этим оператором? - person ms8; 07.06.2015
comment
Можем ли мы получить более быстрый ввод-вывод для этой программы? - person ms8; 07.06.2015

Вы должны использовать Stream и стандартную сортировку:

static String[] sort(File[] files, boolean parallel) {
    return (parallel ? Stream.of(files).parallel() : Stream.of(files))
        .flatMap(f -> {
            try {
                return Files.lines(f.toPath());
            } catch (Exception e) {
                e.printStackTrace();
                return null;
            }
        })
        .sorted()
        .toArray(String[]::new);
}

static String[] sort(File[] files) {
    return sort(files, false);
}

static String[] threadSort(File[] files) {
    return sort(files, true);
}

В моей среде threadSort работает быстрее.

sort:
files=511 sorted lines=104419 elapse=4784ms
threadSort:
files=511 sorted lines=104419 elapse=3060ms
person saka1029    schedule 07.06.2015
comment
Я хочу использовать только два моих метода. - person ms8; 07.06.2015
comment
Можем ли мы получить более быстрый ввод-вывод для этой программы? - person ms8; 07.06.2015
comment
Немного быстрее вывести ввод-вывод за пределы распараллеливания, как это сделали вы. Но сортировать каждый файл медленнее. - person saka1029; 07.06.2015

Вы можете использовать java.util.concurrent.ExecutorService, который будет запускать все ваши задачи в указанном количестве потоков, и как только все потоки закончат выполнение, вы получите объект списка Future, который будет содержать результат выполнения каждого потока. Список будущих объектов будет в том же порядке, в котором вы вставили вызываемые объекты в его список.

Для этого в первую очередь вам понадобится SortingThread реализовать Callable интерфейс, чтобы вы могли получать результат выполнения каждого потока.
Каждый Callable объект должен реализовывать метод call(), а его возвращаемый тип будет вашим Future объектом.

    public class SortingThread implements Callable<String[]> {
    String[] data;
    SortingThread(String[] data) {
        this.data = data;
    }
    @Override
    public String[] call() throws Exception {
        data = MergeSort.mergeSort(data);
        return data;
    }  
   }

Далее вам нужно использовать ExecutorSerivce для управления потоками.

public static String[] sortingExampleWithMultiThreads(File[] files) throws IOException {
      String sortedData[] = new String[0]; 
      int counter = 0; 
      boolean allThreadsTerminated = false;
      SortingThread[] threadList = new SortingThread[files.length];
      ArrayList<Callable<String[]>> callableList = new ArrayList<Callable<String[]>>();
      for (File file : files) {
          String[] data = getData(file);
          callableList.add(new SortingThread(data));  //Prepare a Callable list which would be passed to invokeAll() method.
          counter++;
      }

      ExecutorService service = Executors.newFixedThreadPool(counter);  // Create a fixed size thread pool, one thread for each file processing...
      List<Future<String[]>> futureObjects = service.invokeAll(callableList);  //List of what call() method of SortingThread is returning...

      for(counter=0; counter<files.length; counter++) {
          sortedData = MergeSort.merge(sortedData, futureObjects.get(counter));
      }
      return sortedData;
 }

Таким образом вы можете избежать использования цикла WHILE, который, как известно, увеличивает загрузку ЦП (следовательно, снижает скорость), и если у вас одноядерный ЦП, то он может достичь 100% использования, а если двухъядерный, то 50%.
Кроме того, использование ExecutorService для управления потоками является лучшим способом при работе с многопоточностью вместо того, чтобы разработчик запускал и отслеживал потоки для получения результатов. Таким образом, вы можете рассчитывать на производительность.

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

P.S.: Чтобы получить аккуратные и точные результаты при измерении производительности, всегда создавайте новый экземпляр JVM для каждого запуска.

person hagrawal    schedule 07.06.2015
comment
Можем ли мы получить более быстрый ввод-вывод для этой программы? - person ms8; 07.06.2015
comment
Мы должны использовать потоки, чтобы выполнить работу, теперь нет более быстрых и более медленных потоков. Итак, лучшее, что я могу предложить, это: 1. Используйте собственный API-интерфейс ExecutorService для Java, чтобы вы ожидали наилучшего результата. 2. Все эти многопоточные API, такие как ExecutorService, ThreadPool и т. д., не используют преимущества всех доступных процессоров в ЦП, поэтому лучшим подходом может быть использование фреймворка fork-join (docs.oracle.com/javase/tutorial/essential/concurrency/), который позволяет вам воспользоваться всеми преимуществами доступный процессор вместе с многопоточностью... - person hagrawal; 07.06.2015