Parallel.ForEach и foreach возвращают разные ссылочные переменные

(Обновлено: если название сбивает с толку, я все уши для лучшего)

В настоящее время я работаю над небольшим проектом для класса на С#, и я столкнулся с чем-то странным. Цель проекта — подсчитать все папки, файлы и размер файлов в заданном каталоге, как в 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

Почему такое несоответствие?

Порядок, в котором подсчитываются каталоги/файлы, варьируется между параллельным и foreach. В какой-то момент вы можете столкнуться с UnauthorizedExceptions, где ваш подсчет остановится. Также может быть хорошей идеей использовать Interlocked.Increment для ваших счетчиков в parallel.foreach, чтобы избежать состояния гонки.

SigiN 09.02.2023 21:50

это определенно то, что мне нужно принять во внимание. Тем не менее, этот каталог небольшой и не должен иметь каких-либо из этих проблем (поэтому в консоли ничего не написано о несанкционированном доступе)

Aidan Mellin 09.02.2023 21:52

как сказано в моем редактировании, оформить заказ Interlocked.Increment

SigiN 09.02.2023 21:54

Для чего это стоит: я очень сомневаюсь, что вы получите прирост производительности, пытаясь распараллелить эту конкретную проблему. Накладные расходы на управление потоками, вероятно, будут намного больше, чем любые преимущества многопоточности, которые вы получите.

StriplingWarrior 09.02.2023 23:16

По теме: Параллельный обход дерева в C#. @StriplingWarrior и я опубликовали недавние ответы на этот вопрос.

Theodor Zoulias 10.02.2023 18:37
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
91
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Рекурсивная функция присваивает относительную сумму фактической сумме.

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
  • Параллельная ветвь для w/x/ устанавливает общее количество файлов в 8.
  • Параллельная ветвь для w/y/ устанавливает общее количество файлов в 15.
  • Параллельная ветвь для w/z/ устанавливает общее количество файлов в 12.

Каждая параллель отменяет работу предыдущей.

Вместо этого заставьте рекурсивную функцию подсчитывать относительные файлы, а затем добавлять к итогу вместо переназначения им. См. ниже — обратите внимание, что я переименовал ваши переменные в 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;
}

Эта реализация зависит от условий гонки. Каждый += переводится как ЗАГРУЗИТЬ, ДОБАВИТЬ и СОХРАНИТЬ. Если каждый из двух потоков ЗАГРУЗИТ одно и то же значение, они также будут добавлять и сохранять одно и то же значение.

StriplingWarrior 10.02.2023 00:33

Спасибо - хорошее замечание, хотя условия гонки не являются основной причиной проблемы, это приводит к ошибочному ответу. Я обновил его, чтобы использовать Interlocked

rshepp 10.02.2023 00:51

Другие вопросы по теме