Практическое руководство. Перебор каталогов с файлами с помощью PLINQ

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

Второй запрос использует статические методы EnumerateDirectories и EnumerateFiles, которые сразу начинают возвращать результаты. Этот подход может оказаться быстрее, если вы просматриваете большие деревья каталогов, но разница с первым примером по времени обработки зависит от многих факторов.

Примечание.

Эти примеры предназначены только для демонстрации использования и могут выполняться не быстрее аналогичного последовательного запроса LINQ to Objects. Дополнительные сведения об ускорении см. в статье Общее представление об ускорении выполнения в PLINQ.

Пример GetFiles

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


// Use Directory.GetFiles to get the source sequence of file names.
public static void FileIterationOne(string path)
{
    var sw = Stopwatch.StartNew();
    int count = 0;
    string[]? files = null;
    try
    {
        files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);
    }
    catch (UnauthorizedAccessException)
    {
        Console.WriteLine("You do not have permission to access one or more folders in this directory tree.");
        return;
    }
    catch (FileNotFoundException)
    {
        Console.WriteLine($"The specified directory {path} was not found.");
    }

    var fileContents =
        from FileName in files?.AsParallel()
        let extension = Path.GetExtension(FileName)
        where extension == ".txt" || extension == ".htm"
        let Text = File.ReadAllText(FileName)
        select new
        {
            Text,
            FileName
        };

    try
    {
        foreach (var item in fileContents)
        {
            Console.WriteLine($"{Path.GetFileName(item.FileName)}:{item.Text.Length}");
            count++;
        }
    }
    catch (AggregateException ae)
    {
        ae.Handle(ex =>
        {
            if (ex is UnauthorizedAccessException uae)
            {
                Console.WriteLine(uae.Message);
                return true;
            }
            return false;
        });
    }

    Console.WriteLine($"FileIterationOne processed {count} files in {sw.ElapsedMilliseconds} milliseconds");
}

Пример EnumerateFiles

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

public static void FileIterationTwo(string path) //225512 ms
{
    var count = 0;
    var sw = Stopwatch.StartNew();
    var fileNames =
        from dir in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories)
        select dir;

    var fileContents =
        from FileName in fileNames.AsParallel()
        let extension = Path.GetExtension(FileName)
        where extension == ".txt" || extension == ".htm"
        let Text = File.ReadAllText(FileName)
        select new
        {
            Text,
            FileName
        };
    try
    {
        foreach (var item in fileContents)
        {
            Console.WriteLine($"{Path.GetFileName(item.FileName)}:{item.Text.Length}");
            count++;
        }
    }
    catch (AggregateException ae)
    {
        ae.Handle(ex =>
        {
            if (ex is UnauthorizedAccessException uae)
            {
                Console.WriteLine(uae.Message);
                return true;
            }
            return false;
        });
    }

    Console.WriteLine($"FileIterationTwo processed {count} files in {sw.ElapsedMilliseconds} milliseconds");
}

Если используется GetFiles, следите за наличием нужных разрешений для всех каталогов в дереве. В противном случае создается исключение и результаты не возвращаются. При использовании EnumerateDirectories в запросе PLINQ довольно трудно обрабатывать исключения ввода-вывода настолько мягко, чтобы можно было продолжить работу. Если код должен обрабатывать исключения с ошибками ввода-вывода или несанкционированного доступа, то мы рекомендуем применить другой подход, описанный в статье Практическое руководство. Перебор каталогов с файлами с помощью параллельного класса.

Если есть риск задержки при операциях ввода-вывода (например, при обращении к файлу по сети), попробуйте один из асинхронных подходов, которые описаны в статье TPL и традиционное асинхронное программирование .NET и в этой записи блога.

См. также