Модераторы: LSD, AntonSaburov
  

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> подсчет файлов в каталоге, рекурсивный подсчет в нескольких потоках 
V
    Опции темы
Pawl
Дата 22.11.2012, 10:02 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Опытный
**


Профиль
Группа: Участник
Сообщений: 649
Регистрация: 22.4.2008
Где: Витебск

Репутация: 7
Всего: 28



Уважаемые форумчане, столкнулся с задачей, в которой требуется подсчитать количество файлов в каталоге с помощью рекурсии, в нескольких потоках. Я решил воспользоваться шаблоном fork/join, и вот что у меня получилось:
Код

import java.io.File;
import java.util.ArrayList;
import java.util.concurrent.*;

public class FilesCount extends RecursiveTask<Integer> {
    private static int c = 0;
    private String path;
        
    public FilesCount(String path) {
        this.path = path;
    }
    
    public Integer compute() throws NullPointerException {
    ArrayList<FilesCount> tasks = new ArrayList<>();
    for (File file : new File(path).listFiles()) {                
        if (file.isDirectory()) {
            tasks.add(new FilesCount(file.getPath()));
        } else {
            ++c;
        }
    
//    System.out.println(Thread.currentThread().getName() + " " + path + " " + c);                                            
    }
    invokeAll(tasks);
    return c;                                                
    }
    
    public static void main(String[] args) {         
        try {
    ForkJoinPool pool = new ForkJoinPool();
    int result = pool.invoke(new FilesCount("d:/progs/hibernate"));                  
         System.out.println(result);
        } catch (Exception e) {
         System.out.println("Wrong Directory");
        }
    }    
}

В моем понимании это работает так: сначала просматривается заданный каталог, находящийся по пути path. Если в нем встретится файл, количество файлов (статичесакя переменная с) увеличивается на 1, а если каталог, в список задач попадает новая задача, уже с путем к подкаталогу. Далее эти задачи запускаются, и процесс идет, пока не будет просмотрены все подкаталоги данного каталога. Вроде все прекрасно, и количество файлов считается, но проблема в том, что оно иногда получается немного меньшим, чем есть. Не могу разобраться, почему так происходит, поэтому был бы рад, если кто-нибудь сможет это объяснить.
Спасибо!


--------------------
В действительности всё совсем не так, как на самом деле
PM MAIL   Вверх
jk1
Дата 22.11.2012, 15:34 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Участник
Сообщений: 1168
Регистрация: 17.10.2008
Где: Санкт-Петербург

Репутация: 40
Всего: 75



Имхо все довольно прозаично: статическая переменная, доступ к ней не синхронизирован никак, запись конкурентная из нескольких исполняющихся задач. А инкремент, как известно, не атомарный.

Вообще FJF используется очень странно: fork отсутсвует, join тоже. Идея у этого фреймворка во многом похожа на сортировку слиянием: результаты рекурсивных вызовов нужно мержить (в вашем случае - числа складывать) в вызывающем контексте. А никак не копить в статической переменной. 

Собирая все это вместе получаем примерно следующее:

Код

public class ForkJoinTask extends RecursiveTask<Integer> {
    private String path;

    public ForkJoinTask(String path) {
        this.path = path;
    }

    public Integer compute() throws NullPointerException {
        File file = new File(path);
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            ArrayList<ForkJoinTask> tasks = new ArrayList<ForkJoinTask>(files.length);
            for (File child : file.listFiles()) {
                ForkJoinTask task = new ForkJoinTask(child.getAbsolutePath());
                tasks.add(task);
                task.fork();
            }
            int result = 0;
            for (ForkJoinTask task : tasks) {
                result += task.join();
            }
            return result;
        } else {
            return 1;
        }
    }


    public static void main(String[] args) {
        try {
            ForkJoinPool pool = new ForkJoinPool();
            int result = pool.invoke(new ForkJoinTask("d:/Dropbox"));
            System.out.println(result);
        } catch (Exception e) {
            System.out.println("Wrong Directory");
        }
    }
}


Добавлю, что это не самый эффективный по производительности вариант, но самый простой и для иллюстрации правильного использования подойдет.




--------------------
Opinions are like assholes — everybody has one
PM MAIL   Вверх
Pawl
Дата 22.11.2012, 22:39 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Опытный
**


Профиль
Группа: Участник
Сообщений: 649
Регистрация: 22.4.2008
Где: Витебск

Репутация: 7
Всего: 28



Спасибо за объяснение и наглядный пример. Свой я делал по образцу из книги "Beginning Java 7" by Jeff Friesen, Listing 6-9. Multiplying two matrixes via the Fork/Join Framework. Там также вместо fork и join использовался invokeAll. Как я понял, этот метод объединяет в себе 2 предыдущих:
Цитата

Forks all tasks in the specified collection, returning when isDone 
?
Цитата(jk1 @  22.11.2012,  15:34 Найти цитируемый пост)
А инкремент, как известно, не атомарный.

Вот как. Теперь и мне это известно!   smile 
И все-же мне непонятно, почему при отсутствии синхронизации файлы именно недосчитываются? Я понимаю, если бы выводилось число большее, чем реальное количество файлов: к примеру, один поток может увеличить с дважды, а тут... получается, что в некоторых задачах в методе compute ветка else вообще никогда не задействуется? Если не трудно, можно об этом поподробнее, пожалуйста.




--------------------
В действительности всё совсем не так, как на самом деле
PM MAIL   Вверх
jk1
Дата 23.11.2012, 11:17 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Участник
Сообщений: 1168
Регистрация: 17.10.2008
Где: Санкт-Петербург

Репутация: 40
Всего: 75



Цитата

Там также вместо fork и join использовался invokeAll. Как я понял, этот метод объединяет в себе 2 предыдущих:

Объединяет, только вот API у него неудобное для возвращения результатов. Правильная реализация на нем выглядит примерно так:

Код

 public Integer compute()  {
        File file = new File(path);
        if (file.isDirectory()) {
            File[] files = file.listFiles();
            ArrayList<ForkJoinTask> tasks = new ArrayList<ForkJoinTask>(files.length);
            for (File child : file.listFiles()) {
                tasks.add(new ForkJoinTask(child.getAbsolutePath()));
            }
            invokeAll(tasks);
            int result = 0;
            for (ForkJoinTask task : tasks) {
                try {
                    result += task.get();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
            return result;
        } else {
            return 1;
        }
    }


Как можно видеть результаты потом можно получить только через стандартный Future'овский get() со всеми его исключениями.

Цитата

И все-же мне непонятно, почему при отсутствии синхронизации файлы именно недосчитываются? Я понимаю, если бы выводилось число большее, чем реальное количество файлов: к примеру, один поток может увеличить с дважды


С точностью до наоборот. Насчитать больше реального он не может (с чего бы одному потоку прибавлять дважды? в коде явно указан один инкремент на таск), а насчитать меньше - пожалуйста. Пусть у нас есть два потока, А и B. Инкремент состоит из операции чтения и операции записи увеличенного значения.  Тогда реальная последовательность операций на железе может быть такой:

(текущее значение счетчика = 0)
А: читает текущее значение 0
B: читает текущее значение 0
A: увеличивает считанное значение до 1 и выполняет запись
(текущее значение счетчика теперь = 1. Поток B может видеть это, может не видеть, но это неважно - он уже все считал ранее)
В: увеличивает ранее считанный 0 до 1 и выполняет запись
(текущее значение счетчика = 1)

В итоге в обоих потоках отработал инкремент, но в счетчике лежит не 2, а 1. 



--------------------
Opinions are like assholes — everybody has one
PM MAIL   Вверх
Pawl
Дата 23.11.2012, 21:22 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Опытный
**


Профиль
Группа: Участник
Сообщений: 649
Регистрация: 22.4.2008
Где: Витебск

Репутация: 7
Всего: 28



Цитата(jk1 @  23.11.2012,  11:17 Найти цитируемый пост)
(текущее значение счетчика = 0)А: читает текущее значение 0B: читает текущее значение 0A: увеличивает считанное значение до 1 и выполняет запись(текущее значение счетчика теперь = 1. Поток B может видеть это, может не видеть, но это неважно - он уже все считал ранее)В: увеличивает ранее считанный 0 до 1 и выполняет запись(текущее значение счетчика = 1)

Действительно, весьма доступно, спасибо!
Оффтоп:
Кажется, Эйнштейн говорил, что если вы в чем-то действительно разбираетесь, то сможете объяснить это своей бабушке. Так вот, думаю, Вы сможете smile 
А по теме, я тут проделал эксперимент (для общего развития) - объявил с как private static AtomicInteger c = new AtomicInteger(), вместо ++с написал c.incrementAndGet() и вернул c.intValue(). Результат выдает всегда правильный.
Кстати, о развитии. Хочу все-же FJF лучше освоить, поэтому, если можете подкинуть несколько типовых задач или подсказать, где их взять, был бы очень благодарен.


--------------------
В действительности всё совсем не так, как на самом деле
PM MAIL   Вверх
jk1
Дата 25.11.2012, 22:07 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Участник
Сообщений: 1168
Регистрация: 17.10.2008
Где: Санкт-Петербург

Репутация: 40
Всего: 75



Цитата

Так вот, думаю, Вы сможете smile 


Спасибо

Цитата

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


Задач можно придумать много, например merge sort написать на FJF или программу, выкачивающую на локальную машину все страницы сайта вместе с ассоциированными ресурсами. Но если Вы хотите научиться с пользой применять FJF на практике, то следует подумать вот о чем: распараллеливание несет с собой накладные расходы на управление очередями задач и переключение контекста. Чем больше задач, тем больше overhead. В Вашем примере с подсчетом файлов конкретная задача на исполнение очень мелкая, по сути она сводится к return 1. Поэтому такая реализация будет медленнее даже однопоточного линейного обхода. Чтобы действительно выигрывать в производительности необходимо уметь правильно выбирать размер элементарной задачи - задачи, которая будет выполнятся непосредственно без дальнейшего разбиения. Если выбрать её слишком большой, то ядра будут простаивать. Если выбрать её слишком маленькой, то основное время будет уходить на жонглирование потоками и задачами, а не на полезную работу.


--------------------
Opinions are like assholes — everybody has one
PM MAIL   Вверх
Pawl
Дата 26.11.2012, 11:09 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Опытный
**


Профиль
Группа: Участник
Сообщений: 649
Регистрация: 22.4.2008
Где: Витебск

Репутация: 7
Всего: 28



Цитата(jk1 @  25.11.2012,  22:07 Найти цитируемый пост)
Но если Вы хотите научиться с пользой применять FJF 

Конечно, именно применять и именно с пользой! smile 
Цитата(jk1 @  25.11.2012,  22:07 Найти цитируемый пост)
распараллеливание несет с собой накладные расходы на управление очередями задач и переключение контекста. Чем больше задач, тем больше overhead

Понятно, просто пока набиваю руку. Вот, некоторые концепции схватываю сразу, а некоторые - хуже. Чувствую, тут я пока толком не въехал, поэтому и пытаюсь применить FJF ко всему подряд.
Цитата(jk1 @  25.11.2012,  22:07 Найти цитируемый пост)
merge sort написать на FJF 

имеется ввиду сортировка слиянием?
Цитата(jk1 @  25.11.2012,  22:07 Найти цитируемый пост)
выкачивающую на локальную машину все страницы сайта вместе с ассоциированными ресурсами. 

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


--------------------
В действительности всё совсем не так, как на самом деле
PM MAIL   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Правила форума "Java"
LSD   AntonSaburov
powerOn   tux
javastic
  • Прежде, чем задать вопрос, прочтите это!
  • Книги по Java собираются здесь.
  • Документация и ресурсы по Java находятся здесь.
  • Используйте теги [code=java][/code] для подсветки кода. Используйтe чекбокс "транслит", если у Вас нет русских шрифтов.
  • Помечайте свой вопрос как решённый, если на него получен ответ. Ссылка "Пометить как решённый" находится над первым постом.
  • Действия модераторов можно обсудить здесь.
  • FAQ раздела лежит здесь.

Если Вам помогли, и атмосфера форума Вам понравилась, то заходите к нам чаще! С уважением, LSD, AntonSaburov, powerOn, tux, javastic.

 
0 Пользователей читают эту тему (0 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | Java: Общие вопросы | Следующая тема »


 




[ Время генерации скрипта: 0.0579 ]   [ Использовано запросов: 22 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.