Task.WhenAll() 包含大量任务

Task.WhenAll() with a large list of tasks

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

关注这个 post 的答案:Run two async tasks in parallel and collect results in .NET 4.5(还有几个类似的答案)。我一直在尝试制作一个异步方法,该方法将获取所有旧文件名和新文件名以及单个文件名的列表 FileClass,然后构建这些 Task<FileClass> 的数组并尝试处理它们通过 Task.WhenAll() 并行。但是运行变成了"Cannot await void"错误。我相信这是由于 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 自动完成,也许我急于制作一个 scratch 应用程序)。其次,一旦代码为 "working",我发现虽然 async 减少了完成它的时间,但它并没有完成整个任务列表(因为它们可能有数千个)然后继续该过程的下一阶段。这导致了 Task.Run(() => ReplaceFileNamesAsync()).Wait() 调用,实际上比嵌套循环方法花费的时间更长。将结果解包并合并回 _fileClasses 属性 也需要一些逻辑,这导致了问题。

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

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

Task.WhenAll

Creates a task that will complete when all of the supplied tasks have completed.

但是,这看起来更像是 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;
   }
});

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

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