Практическое руководство. Перебор каталогов с файлами с помощью параллельного класса
Во многих случаях итерация файла является операцией, которая с легкостью выполняется параллельно. В теме Практическое руководство. Перебор каталогов с файлами с помощью PLINQ показан самый простой способ выполнения этой задачи, подходящий для большинства сценариев. Однако сложности могут возникнуть, если коду предстоит обработка многих типов исключений, которые могут создаваться при входе в файловую систему. В следующем примере показан один из возможных способов решения этой проблемы. Для перебора по всем файлам и папкам определенного каталога используется итерация на основе стека, и код может перехватывать и обрабатывать различных исключения. Но, конечно же, способ обработки исключений определяет только разработчик.
Пример
В следующем примере итерация по каталогам выполняется последовательно, однако файлы обрабатываются параллельно. Вероятно, это лучшее решение в ситуации, когда на один каталог приходится большое количество файлов. Кроме того, возможна параллельная итерация каталога и последовательный доступ к каждому файлу. Вполне возможно, что параллельное выполнение циклов будет неэффективным, если только не используется специальная система с большим количеством процессоров. Однако в любом случае необходимо тщательно протестировать приложение, чтобы найти лучшее решение.
Imports System
Imports System.Collections.Generic
Imports System.Diagnostics
Imports System.IO
Imports System.Linq
Imports System.Text
Imports System.Threading
Imports System.Threading.Tasks
Module Parallel_File
Sub Main(ByVal args() As String)
TraverseTreeParallelForEach("C:\Program Files", Sub(f)
' For this demo we don't do anything with the data
' except to read it.
Dim data() As Byte = File.ReadAllBytes(f)
' For user interest, although it slows down the operation.
Console.WriteLine(f)
End Sub)
' Keep the console window open.
Console.ReadKey()
End Sub
Public Sub TraverseTreeParallelForEach(ByVal root As String, ByVal action As Action(Of String))
'Count of files traversed and timer for diagnostic output
Dim fileCount As Integer = 0
Dim sw As Stopwatch = Stopwatch.StartNew()
' Use this value to determine whether to parallelize
' file processing on each folder.
Dim procCount As Integer = System.Environment.ProcessorCount
' Data structure to hold names of subfolders to be
' examined for files.
Dim dirs As Stack(Of String) = New Stack(Of String)
If System.IO.Directory.Exists(root) = False Then
Throw New ArgumentException()
End If
dirs.Push(root)
While (dirs.Count > 0)
Dim currentDir As String = dirs.Pop()
Dim subDirs() As String = Nothing
Dim files() As String = Nothing
Try
subDirs = System.IO.Directory.GetDirectories(currentDir)
' An UnauthorizedAccessException exception will be thrown if we do not have
' discovery permission on a folder or file. It may or may not be acceptable
' to ignore the exception and continue enumerating the remaining files and
' folders. It is also possible (but unlikely) that a DirectoryNotFound exception
' will be raised. This will happen if currentDir has been deleted by
' another application or thread after our call to Directory.Exists. The
' choice of which exceptions to catch depends entirely on the specific task
' you are intending to perform and also on how much you know with certainty
' about the systems on which this code will run.
Catch e As UnauthorizedAccessException
Console.WriteLine(e.Message)
Continue While
Catch e As System.IO.DirectoryNotFoundException
Console.WriteLine(e.Message)
Continue While
End Try
Try
files = System.IO.Directory.GetFiles(currentDir)
Catch e As UnauthorizedAccessException
Console.WriteLine(e.Message)
Continue While
Catch e As System.IO.DirectoryNotFoundException
Console.WriteLine(e.Message)
Continue While
End Try
' Perform the required action on each file here in parallel
' if there are a sufficient number of files in the directory
' or else sequentially if not. Files are opened and processed
' synchronously but this could be modified to perform async I/O.
Try
If files.Length < procCount Then
For Each file In files
action(file)
fileCount = fileCount + 1
Next
Else
Parallel.ForEach(files, Function() 0, Function(file, loopState, localCount)
action(file)
localCount = localCount + 1
Return CType(localCount, Integer)
End Function,
Sub(c)
Interlocked.Exchange(fileCount, fileCount + c)
End Sub)
End If
Catch ae As AggregateException
ae.Handle(Function(ex)
If TypeOf (ex) Is UnauthorizedAccessException Then
' Here we just output a message and go on.
Console.WriteLine(ex.Message)
Return True
End If
' Handle other exceptions here if necessary...
Return False
End Function)
End Try
' Push the subdirectories onto the stack for traversal.
' This could also be done before handing the files.
For Each str As String In subDirs
dirs.Push(str)
Next
' For diagnostic purposes.
Console.WriteLine("Processed {0} files in {1} milleseconds", fileCount, sw.ElapsedMilliseconds)
End While
End Sub
End Module
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Parallel_File
{
class Program
{
static void Main(string[] args)
{
TraverseTreeParallelForEach(@"C:\Program Files", (f) =>
{
// For this demo we don't do anything with the data
// except to read it.
byte[] data = File.ReadAllBytes(f);
// For user interest, although it slows down the operation.
Console.WriteLine(f);
});
// Keep the console window open.
Console.ReadKey();
}
public static void TraverseTreeParallelForEach(string root, Action<string> action)
{
//Count of files traversed and timer for diagnostic output
int fileCount = 0;
var sw = Stopwatch.StartNew();
// Use this value to determine whether to parallelize
// file processing on each folder.
int procCount = System.Environment.ProcessorCount;
// Data structure to hold names of subfolders to be
// examined for files.
Stack<string> dirs = new Stack<string>();
if (!System.IO.Directory.Exists(root))
{
throw new ArgumentException();
}
dirs.Push(root);
while (dirs.Count > 0)
{
string currentDir = dirs.Pop();
string[] subDirs = null;
string[] files = null;
try
{
subDirs = System.IO.Directory.GetDirectories(currentDir);
}
// An UnauthorizedAccessException exception will be thrown if we do not have
// discovery permission on a folder or file. It may or may not be acceptable
// to ignore the exception and continue enumerating the remaining files and
// folders. It is also possible (but unlikely) that a DirectoryNotFound exception
// will be raised. This will happen if currentDir has been deleted by
// another application or thread after our call to Directory.Exists. The
// choice of which exceptions to catch depends entirely on the specific task
// you are intending to perform and also on how much you know with certainty
// about the systems on which this code will run.
catch (UnauthorizedAccessException e)
{
Console.WriteLine(e.Message);
continue;
}
catch (System.IO.DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
continue;
}
try
{
files = System.IO.Directory.GetFiles(currentDir);
}
catch (UnauthorizedAccessException e)
{
Console.WriteLine(e.Message);
continue;
}
catch (System.IO.DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
continue;
}
// Perform the required action on each file here in parallel
// if there are a sufficient number of files in the directory
// or else sequentially if not. Files are opened and processed
// synchronously but this could be modified to perform async I/O.
try
{
if (files.Length < procCount)
{
foreach (var file in files)
{
action(file);
fileCount++;
}
}
else
{
Parallel.ForEach(files, () => 0, (file, loopState, localCount) =>
{
action(file);
return (int) ++localCount;
},
(c) =>
{
Interlocked.Exchange(ref fileCount, fileCount + c);
});
}
}
catch (AggregateException ae)
{
ae.Handle((ex) =>
{
if (ex is UnauthorizedAccessException)
{
// Here we just output a message and go on.
Console.WriteLine(ex.Message);
return true;
}
// Handle other exceptions here if necessary...
return false;
});
}
// Push the subdirectories onto the stack for traversal.
// This could also be done before handing the files.
foreach (string str in subDirs)
dirs.Push(str);
}
// For diagnostic purposes.
Console.WriteLine("Processed {0} files in {1} milleseconds", fileCount, sw.ElapsedMilliseconds);
}
}
}
В этом примере файловый ввод-вывод синхронный. При работе с большими файлами или при использовании медленного сетевого соединения, возможно, асинхронный доступ к файлам окажется более эффективным. Методы асинхронного ввода-вывода можно использовать совместно с параллельной итерацией. Дополнительные сведения см. в разделе Библиотека параллельных задач и традиционное асинхронное программирование .NET.
Обратите внимание, если исключение возникает в главном потоке, потоки, запускаемые методом ForEach, могут продолжить работу. Чтобы остановить эти потоки, можно задать логическую переменную в обработчиках исключений и проверять ее значение при каждой итерации параллельного цикла. Если значение соответствует возникшему исключению, с помощью переменной ParallelLoopState остановите или прервите цикл. Дополнительные сведения см. в разделе Практическое руководство. Остановка цикла Parallel.For или выход из этого цикла.