(Обновлено: если название сбивает с толку, я все уши для лучшего)
В настоящее время я работаю над небольшим проектом для класса на С#, и я столкнулся с чем-то странным. Цель проекта — подсчитать все папки, файлы и размер файлов в заданном каталоге, как в foreach
, так и в Parallel.ForEach
.
Первоначально я делал это рекурсивной функцией возврата, но у меня были проблемы с базовым случаем, учитывая, что он должен был учитывать все возможные пути кода с возвратами. Затем я переключился на использование параметров ссылки. Ниже у меня есть текущий код для методов.
/*
* Calculate total directories, count of files, and size of all files from
* a given path using a singular foreach. Update passed reference parameters.
*/
static void singRecurse(DirectoryInfo di, ref int countFolder, ref int countFile,
ref long countByte)
{
try{
DirectoryInfo[] directories = di.GetDirectories();
foreach(DirectoryInfo d in directories){
countFolder += 1;
foreach(FileInfo f in d.GetFiles()){
countFile += 1;
countByte += f.Length;
}
singRecurse(d, ref countFolder, ref countFile, ref countByte);
}
} catch (UnauthorizedAccessException){
Console.WriteLine("You do not have access to this directory");
}
}
/*
* Calculate total directories, count of files, and size of all files from
* a given path using a parallel foreach. Update passed reference parameters.
*/
static void parRecurse(DirectoryInfo di, ref int countFolder, ref int countFile,
ref long countByte)
{
int countFolderinLambda = countFolder;
int countFileinLambda = countFile;
long countByteinLambda = countByte;
try{
DirectoryInfo[] directories = di.GetDirectories();
Parallel.ForEach(directories, d => {
countFolderinLambda += 1;
foreach(FileInfo f in d.GetFiles()){
countFileinLambda += 1;
countByteinLambda += f.Length;
}
parRecurse(d, ref countFolderinLambda, ref countFileinLambda,
ref countByteinLambda);
});
} catch (UnauthorizedAccessException){
Console.WriteLine("You do not have access to this directory");
}
countFolder = countFolderinLambda;
countFile = countFileinLambda;
countByte = countByteinLambda;
}
Текущий результат запуска обоих процессов как отдельных процессов приводит к:
Parallel calculated in 44ms
6 folders, 20 files, 250498 bytes
Single calculated in 11ms
8 folders, 25 files, 405153 bytes
Почему такое несоответствие?
это определенно то, что мне нужно принять во внимание. Тем не менее, этот каталог небольшой и не должен иметь каких-либо из этих проблем (поэтому в консоли ничего не написано о несанкционированном доступе)
как сказано в моем редактировании, оформить заказ Interlocked.Increment
Для чего это стоит: я очень сомневаюсь, что вы получите прирост производительности, пытаясь распараллелить эту конкретную проблему. Накладные расходы на управление потоками, вероятно, будут намного больше, чем любые преимущества многопоточности, которые вы получите.
По теме: Параллельный обход дерева в C#. @StriplingWarrior и я опубликовали недавние ответы на этот вопрос.
Рекурсивная функция присваивает относительную сумму фактической сумме.
countFolder = countFolderinLambda;
countFile = countFileinLambda;
countByte = countByteinLambda;
Учтите, что эта функция работает параллельно для нескольких каталогов, и все они присваивают свой номер countFile
, ничего не зная о других параллельных каталогах.
Например,
w/ has 5 files
w/x/ has 3 files
w/y/ has 10 files
w/z/ has 7 files
Каждая параллель отменяет работу предыдущей.
Вместо этого заставьте рекурсивную функцию подсчитывать относительные файлы, а затем добавлять к итогу вместо переназначения им. См. ниже — обратите внимание, что я переименовал ваши переменные в relativeX
и totalX
для большей ясности.
void parRecurse(DirectoryInfo di, ref int totalFolders, ref int totalFiles, ref long totalBytes)
{
int relativeFolders = 0;
int relativeFiles = 0;
long relativeBytes = 0;
try
{
DirectoryInfo[] directories = di.GetDirectories();
Parallel.ForEach(directories, d =>
{
Interlocked.Increment(ref relativeFolders);
foreach (FileInfo f in d.GetFiles())
{
Interlocked.Increment(ref relativeFiles);
Interlocked.Add(ref relativeBytes, f.Length);
}
parRecurse(d, ref relativeFolders, ref relativeFiles, ref relativeBytes);
});
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("You do not have access to this directory");
}
Interlocked.Add(ref totalFolders, relativeFolders);
Interlocked.Add(ref totalFiles, relativeFiles);
Interlocked.Add(ref totalBytes, relativeBytes);
}
Кроме того, после написания различных рекурсивных функций я склоняюсь к созданию класса-контейнера для данных вместо передачи ссылок. Классы передаются по ссылке, что упрощает работу. Что-то вроде этого:
public class FileSystemCountContext
{
private int _directories = 0;
private int _files = 0;
private long _bytes = 0;
public int Directories => _directories;
public int Files => _files;
public long Bytes => _bytes;
public override string ToString()
{
return $"{Directories} directories, {Files} files, {Bytes} bytes";
}
public void IncrementDirectories()
{
Interlocked.Increment(ref _directories);
}
public void IncrementFiles()
{
Interlocked.Increment(ref _files);
}
public void IncrementBytes(long amount)
{
Interlocked.Add(ref _bytes, amount);
}
}
FileSystemCountContext CountFileSystem(DirectoryInfo directory, bool parallel = false, FileSystemCountContext? context = null)
{
if (context == null)
{
context = new();
}
foreach (FileInfo fi in directory.GetFiles())
{
context.IncrementFiles();
context.IncrementBytes(fi.Length);
}
if (parallel)
{
Parallel.ForEach(directory.GetDirectories(), di =>
{
context.IncrementDirectories();
CountFileSystem(di, parallel, context);
});
}
else
{
foreach (DirectoryInfo di in directory.GetDirectories())
{
context.IncrementDirectories();
CountFileSystem(di, parallel, context);
}
}
return context;
}
Эта реализация зависит от условий гонки. Каждый +=
переводится как ЗАГРУЗИТЬ, ДОБАВИТЬ и СОХРАНИТЬ. Если каждый из двух потоков ЗАГРУЗИТ одно и то же значение, они также будут добавлять и сохранять одно и то же значение.
Спасибо - хорошее замечание, хотя условия гонки не являются основной причиной проблемы, это приводит к ошибочному ответу. Я обновил его, чтобы использовать Interlocked
Порядок, в котором подсчитываются каталоги/файлы, варьируется между параллельным и foreach. В какой-то момент вы можете столкнуться с UnauthorizedExceptions, где ваш подсчет остановится. Также может быть хорошей идеей использовать Interlocked.Increment для ваших счетчиков в parallel.foreach, чтобы избежать состояния гонки.