【发布时间】:2020-02-15 10:17:25
【问题描述】:
我一直致力于重构一个过程,该过程迭代具有Filename、NewFilename 和string[] 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(() => ReplaceFileNamesAsync()).Wait() 调用,它实际上比嵌套循环方法花费的时间更长。将结果解包并合并回 _fileClasses 属性也需要一些逻辑,这导致了问题。
Parallel.ForEach 是一个更快的过程,虽然我没有看到下面发布的更新代码,但我最终得到了大致相同的结果(字典除外)。
【问题讨论】:
-
@MichaelRandall 是的,这是我们能够使其始终如一地工作的唯一方法。使用 WhenAll 和 async 方法(删除嵌套循环,但仍然适合每个文件和每个文件引用)在我的笔记本电脑上大约 6.17 个滴答声。这在真正的工作站上绰绰有余。
标签: c# async-await task-parallel-library