Procedimiento para iterar directorios con PLINQ

En este artículo se muestran dos maneras de paralelizar operaciones en directorios de archivos. La primera consulta utiliza el método GetFiles para rellenar una matriz de nombres de archivo en un directorio y en todos los subdirectorios. Este método puede generar latencia al principio de la operación porque no realiza ninguna devolución hasta que se rellena toda la matriz. Sin embargo, una vez que se rellena la matriz, PLINQ puede procesarla en paralelo rápidamente.

La segunda consulta utiliza los métodos estáticos EnumerateDirectories y EnumerateFiles, que empiezan a devolver resultados inmediatamente. Este enfoque puede ser más rápido cuando esté iterando los árboles de directorio de gran tamaño, pero el tiempo de procesamiento en comparación con el primer ejemplo depende de muchos factores.

Nota

La finalidad de estos ejemplos es mostrar el uso, y puede que su ejecución no sea tan rápida como la de la consulta LINQ to Objects secuencial equivalente. Para más información sobre la velocidad, vea Introducción a la velocidad en PLINQ.

Ejemplo de GetFiles

En el ejemplo siguiente se muestra cómo iterar directorios de archivos en escenarios sencillos cuando se tiene acceso a todos los directorios del árbol, los tamaños de archivo no son grandes y los tiempos de acceso no son significativos. Este enfoque implica un período de latencia al principio, mientras que la matriz de nombres de archivo se está construyendo.


// 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");
}

Ejemplo de EnumerateFiles

En el ejemplo siguiente se muestra cómo iterar directorios de archivos en escenarios sencillos cuando se tiene acceso a todos los directorios del árbol, los tamaños de archivo no son grandes y los tiempos de acceso no son significativos. Este enfoque comienza con la generación de resultados con más rapidez que en el ejemplo anterior.

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");
}

Cuando se usa GetFiles, asegúrese de que tiene suficientes permisos en todos los directorios del árbol. De lo contrario, se producirá una excepción y no se devolverá ningún resultado. Cuando se usa EnumerateDirectories en una consulta PLINQ, resulta problemático controlar las excepciones de E/S de una forma correcta que le permita continuar la iteración. Si el código debe administrar E/S o excepciones de acceso no autorizado, debe considerar el enfoque descrito en Procedimiento Iteración de directorios de archivos con la clase Parallel.

Si la latencia de E/S es un problema (por ejemplo, con E/S de archivos a través de una red), valore la posibilidad de usar una de las técnicas de E/S asincrónicas descritas en TPL y la programación asincrónica tradicional de .NET Framework y en esta entrada de blog.

Vea también