多くの場合、ファイル反復処理は簡単に並列化できる操作です。 「方法: PLINQ を使用してファイル ディレクトリを反復処理する」のトピックは、多くのシナリオでこのタスクを実行するための簡単な方法を示しています。 ただし、ファイル システムへのアクセス時に発生する可能性のある多くの種類の例外をコードで処理する必要がある場合は、複雑さが生じることがあります。 次の例は、この問題への対処方法の 1 つを示しています。 この例では、スタック ベースの反復処理を使用して、指定されたディレクトリにあるすべてのファイルとフォルダーを走査し、コードで各種例外をキャッチして処理できるようにしています。 例外を処理する方法は開発者に委ねられています。
例
次の例では、ディレクトリの反復処理は順次実行されますが、ファイルの処理は並列で実行されます。 これは、ディレクトリのファイル占有率が大きい場合に最適な方法と考えられます。 また、ディレクトリの反復処理を並列化し、各ファイルに順次アクセスすることもできます。 多数のプロセッサを搭載したコンピューターを明確に対象としている場合を除き、両方のループを並列化するのは効率的とは言えません。 ただし、どの場合もアプリケーションを徹底的にテストして、最適な方法を決定する必要があります。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Security;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
try
{
TraverseTreeParallelForEach(@"C:\Program Files", (f) =>
{
// Exceptions are no-ops.
try
{
// Do nothing with the data except read it.
byte[] data = File.ReadAllBytes(f);
}
catch (FileNotFoundException) { }
catch (IOException) { }
catch (UnauthorizedAccessException) { }
catch (SecurityException) { }
// Display the filename.
Console.WriteLine(f);
});
}
catch (ArgumentException)
{
Console.WriteLine(@"The directory 'C:\Program Files' does not exist.");
}
// 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();
// Determine whether to parallelize file processing on each folder based on processor count.
int procCount = Environment.ProcessorCount;
// Data structure to hold names of subfolders to be examined for files.
Stack<string> dirs = new Stack<string>();
if (!Directory.Exists(root))
{
throw new ArgumentException(
"The given root directory doesn't exist.", nameof(root));
}
dirs.Push(root);
while (dirs.Count > 0)
{
string currentDir = dirs.Pop();
string[] subDirs = { };
string[] files = { };
try
{
subDirs = Directory.GetDirectories(currentDir);
}
// Thrown if we do not have discovery permission on the directory.
catch (UnauthorizedAccessException e)
{
Console.WriteLine(e.Message);
continue;
}
// Thrown if another process has deleted the directory after we retrieved its name.
catch (DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
continue;
}
try
{
files = Directory.GetFiles(currentDir);
}
catch (UnauthorizedAccessException e)
{
Console.WriteLine(e.Message);
continue;
}
catch (DirectoryNotFoundException e)
{
Console.WriteLine(e.Message);
continue;
}
catch (IOException e)
{
Console.WriteLine(e.Message);
continue;
}
// Execute in parallel if there are enough files in the directory.
// Otherwise, execute sequentially.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.Add(ref 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 {fileCount} files in {sw.ElapsedMilliseconds} milliseconds");
}
}
Imports System.Collections.Generic
Imports System.Diagnostics
Imports System.IO
Imports System.Security
Imports System.Threading
Imports System.Threading.Tasks
Module Example
Sub Main()
Try
TraverseTreeParallelForEach("C:\Program Files",
Sub(f)
' Exceptions are No-ops.
Try
' Do nothing with the data except read it.
Dim data() As Byte = File.ReadAllBytes(f)
' In the event the file has been deleted.
Catch e As FileNotFoundException
' General I/O exception, especially if the file is in use.
Catch e As IOException
' Lack of adequate permissions.
Catch e As UnauthorizedAccessException
' Lack of adequate permissions.
Catch e As SecurityException
End Try
' Display the filename.
Console.WriteLine(f)
End Sub)
Catch e As ArgumentException
Console.WriteLine("The directory 'C:\Program Files' does not exist.")
End Try
' 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()
' Determine whether to parallelize file processing on each folder based on processor count.
Dim procCount As Integer = System.Environment.ProcessorCount
' Data structure to hold names of subfolders to be examined for files.
Dim dirs As New Stack(Of String)
If Not Directory.Exists(root) Then Throw New ArgumentException(
"The given root directory doesn't exist.", NameOf(root))
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 = Directory.GetDirectories(currentDir)
' Thrown if we do not have discovery permission on the directory.
Catch e As UnauthorizedAccessException
Console.WriteLine(e.Message)
Continue While
' Thrown if another process has deleted the directory after we retrieved its name.
Catch e As DirectoryNotFoundException
Console.WriteLine(e.Message)
Continue While
End Try
Try
files = Directory.GetFiles(currentDir)
Catch e As UnauthorizedAccessException
Console.WriteLine(e.Message)
Continue While
Catch e As DirectoryNotFoundException
Console.WriteLine(e.Message)
Continue While
Catch e As IOException
Console.WriteLine(e.Message)
Continue While
End Try
' Execute in parallel if there are enough files in the directory.
' Otherwise, execute sequentially.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 += 1
Next
Else
Parallel.ForEach(files, Function() 0, Function(file, loopState, localCount)
action(file)
localCount = localCount + 1
Return localCount
End Function,
Sub(c)
Interlocked.Add(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} milliseconds", fileCount, sw.ElapsedMilliseconds)
End While
End Sub
End Module
この例では、ファイル I/O を同期的に実行します。 大きなファイルを処理する場合やネットワーク接続が低速の場合は、ファイルに非同期にアクセスするよりも望ましいと考えられます。 非同期 I/O の手法を並列反復処理と組み合わせることができます。 詳細については、「TPL と従来の .NET 非同期プログラミング」を参照してください。
この例では、ローカル変数 fileCount
を使用して、処理済みファイルの合計数を示すカウントを管理します。 この変数は複数のタスクから同時にアクセスされる可能性があるため、この変数へのアクセスは Interlocked.Add メソッドの呼び出しによって同期されています。
メイン スレッドで例外がスローされた場合、ForEach メソッドによって開始されたスレッドが引き続き実行されることがあります。 これらのスレッドを停止するには、例外ハンドラーで Boolean 変数を設定し、並列ループを反復処理するたびに値をチェックします。 例外がスローされたことを値が示している場合は、ParallelLoopState 変数を使用してループを停止または中断します。 詳細については、「方法: Parallel.For ループを停止または中断する」を参照してください。
参照
.NET