Task.WhenAll () с большим списком задач - PullRequest
0 голосов
/ 14 февраля 2020

Я работал над рефакторингом процесса, который выполняет итерацию по коллекции FileClass объектов, имеющих свойства Filename, NewFilename и string[] FileReferences, и заменяет все FileReferences, в которых ссылаются на старое имя файла с новым. Приведенный ниже код несколько упрощен тем, что реальное свойство ссылок на файлы - это не просто список имен файлов - это строки, которые могут содержать имя файла где-то в них или нет. Текущий код в порядке, если в коллекции _fileClass меньше 1000 объектов ... но он мучительно медленный, если есть больше объектов или свойство ссылок на файлы имеет тысячи.

После ответов на этот пост: Запустите две асинхронные c задачи параллельно и соберите результаты в. NET 4.5 (и нескольким нравится). Я пытался создать asyn c метод, который бы брал список всех старых и новых имен файлов, а также отдельный FileClass, затем создавал массив из этих Task<FileClass> и пытался обработать их в параллельно через Task.WhenAll(). Но возникает ошибка «Cannot a awit void». Я считаю, что это связано с Task.Run(() => ...);, но удаление () => вызывает дальнейшие проблемы.

Это более старая кодовая база, и я не могу позволить асину c распространяться дальше, чем вызывающий код (в данном случае Main, как я нашел в некоторых других примерах. Также я не могу используйте asyn c foreach в C # 8 из-за ограничения. Net 4.5.

class Program
    {
        private static List<FileClass> _fileClasses;

        static void Main(string[] args)
        {
            var watch = new Stopwatch();

            _fileClasses = GetFileClasses();

            watch.Start();
            ReplaceFileNamesAsync();
            watch.Stop();

            Console.WriteLine($"Async Elapsed Ticks: {watch.ElapsedTicks}");

            watch.Reset();

            //watch.Start();
            //ReplaceFileNamesSLOW();
            //watch.Stop();

            //Console.WriteLine($"Slow Elapsed Ticks: {watch.ElapsedTicks}");

            Console.ReadLine();
        }

        public static async void ReplaceFileNamesAsync()
        {
            var newOldFilePairs = _fileClasses.Select(p => new NewOldFilePair() { OldFile = p.Filename, NewFile = p.NewFilename }).ToArray();

            var tasks = new List<Task<FileClass>>();

            foreach (var file in _fileClasses)
            {
                tasks.Add(ReplaceFileNamesAsync(newOldFilePairs, file));
            }

            //Red underline "Cannot await void".
            FileClass[] result = await Task.WaitAll(tasks.ToArray());
        }

        private static async Task<FileClass> ReplaceFileNamesAsync(NewOldFilePair[] fastConfigs, FileClass fileClass)
        {
            foreach (var config in fastConfigs)
            {
                //I suspect this is part of the issue.
                await Task.Run(() => fileClass.ReplaceFileNamesInFileReferences(config.OldFile, config.NewFile));
            }

            return fileClass;
        }

        public static void ReplaceFileNamesSLOW()
        {
            // Current Technique
            for (var i = 0; i < _fileClasses.Count; i++)
            {
                var oldName = _fileClasses[i].Filename;
                var newName = _fileClasses[i].NewFilename;

                for (var j = 0; j < _fileClasses.Count; j++)
                {
                    _fileClasses[j].ReplaceFileNamesInFileReferences(oldName, newName);
                }
            }
        }

        public static List<FileClass> GetFileClasses(int numberToGet = 2000)
        {
            //helper method to build a bunch of FileClasses
            var fileClasses = new List<FileClass>();

            for (int i = 0; i < numberToGet; i++)
            {
                fileClasses.Add(new FileClass()
                {
                    Filename = $@"C:\fake folder\fake file_{i}.ext",
                    NewFilename = $@"C:\some location\sub folder\fake file_{i}.ext"
                });
            }

            var fileReferences = fileClasses.Select(p => p.Filename).ToArray();

            foreach (var fileClass in fileClasses)
            {
                fileClass.FileReferences = fileReferences;
            }

            return fileClasses;
        }
    }

    public class NewOldFilePair
    {
        public string OldFile { get; set; }
        public string NewFile { get; set; }
    }

    public class FileClass
    {
        public string Filename { get; set; }
        public string NewFilename { get; set; }
        public string[] FileReferences { get; set; }

        //Or this might be the void it doesn't like.
        public void ReplaceFileNamesInFileReferences(string oldName, string newName)
        {
            if (FileReferences == null) return;
            if (FileReferences.Length == 0) return;

            for (var i = 0; i < FileReferences.Length; i++)
            {
                if (FileReferences[i] == oldName) FileReferences[i] = newName;
            }
        }
    }

Обновление Если другие найдут этот вопрос и действительно будут нуждаться в реализации чего-то подобного выше, Были некоторые потенциальные подводные камни, о которых стоит упомянуть. Очевидно, у меня была опечатка для Task.WaitAll() против Task.WhenAll() (я обвиняю VS в автозаполнении, и, возможно, я тороплюсь сделать скретч-приложение ?). Во-вторых, как только код «заработал», Я обнаружил, что в то время как asyn c сокращал время, чтобы пройти через это, он не завершал весь список задач (как они могут быть в тысячах), прежде чем перейти к следующему этапу процесса. вызов Task.Run(() => ReplaceFileNamesAsync()).Wait(), который на самом деле занял больше времени, чем вложенный метод l oop. Для распаковки и объединения результатов обратно в свойство _fileClasses также потребовалось несколько логических операций c, что способствовало

* Parallel.ForEach был гораздо более быстрым процессом, и хотя я не увидел обновленный код, опубликованный ниже, я получил в основном тот же результат (кроме словаря).

Ответы [ 2 ]

3 голосов
/ 14 февраля 2020

Чтобы решить начальную проблему, вы должны использовать await Task.WhenAll, а не Task.WaitAll

Task.WhenAll

Создает задачу, которая будет завершить, когда все поставленные задачи завершены.

Однако это выглядит как большая часть задания для Parallel.ForEach

Другая проблема заключается в том, что вы перебираете один и тот же список дважды (вложенный ), который является квадратиком c сложность времени и определенно не поточно-ориентированный

В качестве решения вы можете создать словарь изменений, l oop поверх изменения, установленного один раз (параллельно), и обновите ссылки в одном go.

_fileClasses = GetFileClasses();

// create a dictionary for fast lookup
var changes = _fileClasses.Where(x => x.Filename != null && x.NewFilename != null)
                          .ToDictionary(x => x.Filename, x => x.NewFilename);

// parallel the workloads
Parallel.ForEach(_fileClasses, (item) =>
{
   // iterate through the references
   for (var i = 0; i < item.FileReferences.Length; i++)
   {
      // check for updates
      if (changes.TryGetValue(item.FileReferences[i], out var value))
         item.FileReferences[i] = value;
   }
});

Примечание : Это не было полным решением, так как весь код не был предоставлен, однако сложность по времени должна быть значительной лучше

1 голос
/ 14 февраля 2020

Попробуйте использовать Task.WhenAll. Это позволит вам ждать его, потому что он возвращает задание. Task.WaitAll является блокирующим вызовом и будет ждать завершения всех задач, прежде чем вернуть void.

...