Task.WhenAll() 包含大量任务
Task.WhenAll() with a large list of tasks
我一直致力于重构一个迭代 FileClass
对象集合的过程,这些对象具有 Filename
、NewFilename
和 string[] 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
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。
我一直致力于重构一个迭代 FileClass
对象集合的过程,这些对象具有 Filename
、NewFilename
和 string[] 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
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。