如何在Parallel.ForEachAsync中保持源与结果列表的排序一致?
问题描述
使用Parallel.ForEachAsync执行异步文件上传时,功能正常但无法保持源列表的排序顺序。此前使用PLINQ时可通过AsOrdered运算符维持顺序,但Parallel.ForEachAsync更适合异步I/O场景,想知道如何同步源列表与目标列表的排序。
现有Parallel.ForEachAsync实现代码:
await Parallel.ForEachAsync(model.DestinationFiles, new ParallelOptions { MaxDegreeOfParallelism = 20 }, async (file, CancellationToken) => { var storeAsync = await _fileServerService.Init(displayUrl).StoreAsync(file.FileInfo, false, file.OutputFileName); convertResultDto.Files.Add(new ConverterConvertResultFile(storeAsync)); });
之前的PLINQ实现(可保持顺序):
var storeFiles = model.DestinationFiles.AsParallel().AsOrdered().WithDegreeOfParallelism(50) .Select(file => StoreAsync(file.FileInfo, false, file.OutputFileName).GetAwaiter().GetResult()) .Select(storeFile => new StoreFile { FileId = storeFile.FileId, Url = storeFile.Url, OutputFileName = storeFile.OutputFileName, Size = storeFile.Size });
解决方案
Parallel.ForEachAsync本身不会自动维护元素的执行顺序,因为并行任务的完成时间不确定,直接调用List.Add会导致结果列表顺序混乱。可以通过以下两种方式解决:
方法1:为元素绑定原始索引,处理后按索引排序
先将源列表的每个元素与其原始索引绑定,并行处理完成后,再根据索引对结果排序:
// 为每个文件带上原始索引 var indexedFiles = model.DestinationFiles.Select((file, index) => new { File = file, Index = index }).ToList(); var tempResults = new ConcurrentBag<(int Index, ConverterConvertResultFile Result)>(); await Parallel.ForEachAsync(indexedFiles, new ParallelOptions { MaxDegreeOfParallelism = 20 }, async (indexedFile, CancellationToken) => { var storeResult = await _fileServerService.Init(displayUrl).StoreAsync(indexedFile.File.FileInfo, false, indexedFile.File.OutputFileName); tempResults.Add((indexedFile.Index, new ConverterConvertResultFile(storeResult))); }); // 按原始索引排序后加入目标列表 convertResultDto.Files.AddRange(tempResults.OrderBy(item => item.Index).Select(item => item.Result));
方法2:用固定长度数组按索引插入结果
提前创建与源列表长度一致的数组,处理完成后直接将结果写入对应索引的位置,最后转成列表:
var resultArray = new ConverterConvertResultFile[model.DestinationFiles.Count]; await Parallel.ForEachAsync(model.DestinationFiles.Select((file, index) => new { File = file, Index = index }), new ParallelOptions { MaxDegreeOfParallelism = 20 }, async (indexedFile, CancellationToken) => { var storeResult = await _fileServerService.Init(displayUrl).StoreAsync(indexedFile.File.FileInfo, false, indexedFile.File.OutputFileName); // 数组单个元素的写入操作是原子性的,线程安全 resultArray[indexedFile.Index] = new ConverterConvertResultFile(storeResult); }); convertResultDto.Files.AddRange(resultArray);
注意事项
- 禁止直接在并行任务中调用
List<T>.Add:List<T>不是线程安全的,不仅会导致顺序混乱,还可能引发数据损坏问题。 - 两种方法的核心都是保留原始索引,通过索引恢复顺序——
Parallel.ForEachAsync无法像PLINQ的AsOrdered那样自动维护执行顺序。
内容的提问来源于stack exchange,提问作者Tomas
相关产品推荐
相关产品推荐

