【问题标题】:Task.WhenAll() with a large list of tasksTask.WhenAll() 包含大量任务
【发布时间】:2020-02-15 10:17:25
【问题描述】:

我一直致力于重构一个过程,该过程迭代具有FilenameNewFilenamestring[] FileReferences 属性的FileClass 对象集合,并替换所有引用的FileReferences旧文件名与新文件名。下面的代码稍微简化了一点,因为真正的文件引用属性不仅仅是文件名的列表——它们是可能包含或不包含文件名的行。当前代码在 _fileClass 集合低于大约 1000 个对象时是可以的......但如果有更多对象,或者文件引用属性有数千个,则速度会非常慢。

按照这篇文章的答案:Run two async tasks in parallel and collect results in .NET 4.5(还有几个喜欢它)。我一直在尝试创建一个异步方法,该方法将获取所有旧文件和新文件名的列表以及单个FileClass,然后构建这些Task<FileClass> 的数组并尝试通过@ 并行处理它们987654330@。但遇到“不能等待无效”错误。我相信这是由于Task.Run(() => ...);,但删除() => 会导致更多问题。

这是一个较旧的代码库,我不能让异步传播比调用代码更远(在这种情况下,Main,正如我在其他一些示例中发现的那样。我也不能使用 C#8 的异步 foreach由于 .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 自动完成功能,也许我急于制作一个临时应用程序????)。其次,一旦代码“工作”,我发现虽然 async 减少了完成此任务的时间,但它并没有完成整个任务列表(因为它们可能有数千个),然后继续进入下一个阶段的过程。这导致了Task.Run(() =&gt; ReplaceFileNamesAsync()).Wait() 调用,它实际上比嵌套循环方法花费的时间更长。将结果解包并合并回 _fileClasses 属性也需要一些逻辑,这导致了问题。

Parallel.ForEach 是一个更快的过程,虽然我没有看到下面发布的更新代码,但我最终得到了大致相同的结果(字典除外)。

【问题讨论】:

  • @MichaelRandall 是的,这是我们能够使其始终如一地工作的唯一方法。使用 WhenAll 和 async 方法(删除嵌套循环,但仍然适合每个文件和每个文件引用)在我的笔记本电脑上大约 6.17 个滴答声。这在真正的工作站上绰绰有余。

标签: c# async-await task-parallel-library


【解决方案1】:

要解决最初的问题,您应该使用await Task.WhenAll 而不是Task.WaitAll

Task.WhenAll

创建一个将在所有提供的任务都完成时完成的任务 完成。

不过,这看起来更像是 Parallel.ForEach 的工作

另一个问题是您在同一个列表上循环两次(嵌套),这是 二次时间复杂度,绝对不是 线程安全

作为解决方案,您可以创建一个更改字典,循环一次(并行)更改集,然后一次性更新引用。

_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;
   }
});

注意:这并不是一个完整的解决方案,因为没有提供所有代码,但是时间复杂度应该会好很多

【讨论】:

  • 那行得通。但我想研究一下 Parallel.ForEach...
  • 感谢您花时间整理Parallel.ForEach 示例。今天我得到了非常相似的东西,但是正如我提到的,你注意到,缺乏真正的代码,以及“检查更新”循环中实际发生的事情的复杂性,字典和逻辑不同。由此产生的重构极大地改善了流程——我现在的任务是找到类似的瓶颈来改进?‍♂️
【解决方案2】:

尝试使用Task.WhenAll。它将允许您等待它,因为它返回一个任务。 Task.WaitAll 是一个阻塞调用,会等到所有任务完成后才返回 void。

【讨论】:

    猜你喜欢
    • 2018-02-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-02-02
    • 2016-01-30
    • 2012-11-06
    • 2021-07-17
    • 1970-01-01
    相关资源
    最近更新 更多