繁体   English   中英

Task.WhenAll() 包含大量任务

[英]Task.WhenAll() with a large list of tasks

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

按照这篇文章的答案: 并行运行两个异步任务并在 .NET 4.5 中收集结果(以及几个类似的)。 我一直在尝试创建一个异步方法,该方法将获取所有旧文件名和新文件名的列表以及一个单独的FileClass ,然后构建这些Task<FileClass>的数组并尝试通过Task.WhenAll()并行处理它们Task.WhenAll() 但是遇到“无法等待无效”错误。 我相信这是由于Task.Run(() => ...); 但是删除() =>会导致进一步的问题。

这是一个较旧的代码库,我不能让 async 比调用代码传播得更远(在这种情况下, Main ,正如我在其他一些示例中发现的那样。由于 .Net,我也不能使用 C#8 的 async foreach 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(() => ReplaceFileNamesAsync()).Wait()调用,它实际上比嵌套循环方法花费的时间更长。 将结果解包并合并回_fileClasses属性也需要一些逻辑,这导致了问题。

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

要解决最初的问题,您是否应该使用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;
   }
});

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM