You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 00:05:31