C#中使用Parallel.ForEachAsync异步操作多线程的方法

 更新时间:2026年07月24日 16:57:42   作者:Vae_Mars  
还在用Task.WhenAll导致内存溢出,Parallel.ForEachAsync专为异步并发设计,能轻松控制最大并发数,避免资源耗尽,本文通过实战示例讲解下载、取消和结果收集,帮你掌握C#异步并发编程的正确姿势
,需要的朋友可以参考下

1. 核心概念

Parallel.ForEachAsync 是在 .NET 6 中引入的方法。
它的核心作用是:对一组数据执行异步操作,并且同时控制并发数量(也就是同时跑多少个任务)
它完美解决了两个痛点:

  1. 传统的 Parallel.ForEach 不支持 async/await(它是为阻塞式 CPU 任务设计的)。
  2. 直接用 Task.WhenAll(tasks) 会一次性把所有任务扔出去,如果数据有 10 万条,会瞬间耗尽内存或网络端口。

2. 基本语法与关键参数

public static Task ForEachAsync<TSource>(
    this IEnumerable<TSource> source,
    ParallelOptions parallelOptions,
    Func<TSource, CancellationToken, ValueTask> body
)

关键参数说明

  • source:数据源(如数组、List、或你用 Chunk() 切分出的块)。
  • parallelOptions:并行配置项,极其重要,包含两个核心属性:
    • MaxDegreeOfParallelism最大并发数。比如设为 5,表示最多同时运行 5 个异步任务,完成一个再补一个。
    • CancellationToken:取消令牌,用于中途终止所有并发任务。
  • body:要执行的异步逻辑(注意返回类型是 ValueTask,不是 Task,性能更好)。

3. 经典示例

示例 1:批量并发下载网页/API请求(最常见场景)

假设我们要下载 100 个网页,如果串行可能需要 1 分钟,但如果同时开 10 个并发,只需 6 秒。

using System.Net.Http;
List<string> urls = GetUrls(); // 假设有 100 个 URL
using HttpClient client = new HttpClient();
// 配置并行选项:最大并发 10
var options = new ParallelOptions { MaxDegreeOfParallelism = 10 };
Console.WriteLine("开始并发下载...");
await Parallel.ForEachAsync(urls, options, async (url, ct) =>
{
    try
    {
        // 传入 ct,如果外部取消,当前请求会立即中止
        string content = await client.GetStringAsync(url, ct);
        Console.WriteLine($"成功下载 {url},长度: {content.Length}");
    }
    catch (OperationCanceledException)
    {
        // 被取消
    }
    catch (Exception ex)
    {
        Console.WriteLine($"下载失败 {url}: {ex.Message}");
    }
});
Console.WriteLine("全部下载完成。");

示例二:收集结果

ForEachAsync 本身没有返回值(返回 Task),如果需要收集处理结果,必须使用线程安全集合(如 ConcurrentBag 或 ConcurrentQueue)。**

using System.Collections.Concurrent;
using System.Diagnostics;

public static async Task Example_WithResultCollection()
{
    var inputs = Enumerable.Range(1, 10);
    var results = new ConcurrentBag<int>(); // 线程安全集合

    await Parallel.ForEachAsync(inputs, async i =>
    {
        await Task.Delay(100); // 模拟耗时操作
        int result = i * i;    // 计算逻辑
        results.Add(result);   // 并发添加结果(线程安全)
        
        Console.WriteLine($"{DateTime.Now:mm:ss:fff} - 处理 {i}");
    });

    Console.WriteLine($"结果数量: {results.Count}");
}

示例三:配合取消令牌

在用户关闭窗口或点击“停止”按钮时优雅地停止处理。

public static async Task Example_WithCancellation(CancellationToken cts)
{
    var range = Enumerable.Range(0, 1000);
    
    try
    {
        await Parallel.ForEachAsync(range, 
            new ParallelOptions { CancellationToken = cts, MaxDegreeOfParallelism = 10 }, 
            async (i, ct) =>
            {
                // 如果外部取消,这里会抛出 OperationCanceledException
                await Task.Delay(100, ct); 
                Console.WriteLine($"处理中: {i}");
            });
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("任务被用户取消。");
    }
}

4. 核心优势对比表

特性foreach + awaitTask.WhenAllParallel.ForEachAsync
执行方式串行(一个接一个)全量并发(同时启动所有)可控并发(池化执行)
内存/资源占用极低极高(容易内存溢出/端口耗尽)适中(按需启动)
总耗时最长最短(如果不崩溃的话)较短(与并发数成正比)
支持取消支持支持完美支持

5. 注意事项与避坑指南

必须限制并发数
不设置 MaxDegreeOfParallelism 时,默认可能会使用处理器数量作为并发度。对于网络请求,建议显式设置为 520 之间(根据目标服务器的承受能力和你的网络带宽决定),防止把对方服务器打挂,或触发对方防爬机制被封 IP。

线程安全问题(最容易踩的坑)
在并发环境下,Parallel.ForEachAsync 的内部相当于多线程同时运行。如果你在里面修改了 UI 绑定的集合(如 ObservableCollection)或普通的计数字典,极大概率会报错或数据错乱

  • 集合操作:使用 ConcurrentDictionaryConcurrentBag,或者用 lock
  • UI 更新:如果要在里面更新 WPF/WinForms 的 UI,需要使用 Dispatcher.Invoke

异常处理

  • 如果在循环体内抛出异常(且未 catch 吞掉),Parallel.ForEachAsync 会将异常包装进 AggregateException 抛出。
  • 如果你想让某个任务失败不影响其他任务,就在循环体内部 try-catch 吞掉单次异常。
  • 如果收到 CancellationToken,必须立即停止并抛出 OperationCanceledException

以上就是C#中使用Parallel.ForEachAsync异步操作多线程的方法的详细内容,更多关于C# Parallel.ForEachAsync多线程异步操作的资料请关注脚本之家其它相关文章!

相关文章

  • C#对Windows服务组的启动与停止操作

    C#对Windows服务组的启动与停止操作

    这篇文章主要为大家详细介绍了C#对Windows服务组的启动与停止操作,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-03-03
  • 分享C#操作内存读写方法的主要实现代码

    分享C#操作内存读写方法的主要实现代码

    这篇文章介绍了C#操作内存读写方法的主要实现代码,下面让我们来看看具体的实例实现,有需要的朋友可以参考一下
    2013-08-08
  • C#实现Bitmap类型与Byte[]类型相互转化的示例详解

    C#实现Bitmap类型与Byte[]类型相互转化的示例详解

    在C#编程中,Bitmap类型和Byte[]类型之间的相互转化是图像处理和数据传输中常见的需求,Bitmap类型表示一个位图图像,而Byte[]类型则是一个字节数组,本文将详细介绍如何在这两种类型之间进行相互转化,需要的朋友可以参考下
    2024-07-07
  • C#实现登录窗口(不用隐藏)

    C#实现登录窗口(不用隐藏)

    C#登录窗口的实现,特点就是不用隐藏,感兴趣的朋友不要错过
    2013-11-11
  • C#中WPF依赖属性的正确学习方法

    C#中WPF依赖属性的正确学习方法

    这篇文章主要介绍了C#中WPF依赖属性的正确学习方法 ,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-08-08
  • C#修改图片尺寸以及不改变原有图片比例

    C#修改图片尺寸以及不改变原有图片比例

    文章介绍了如何使用C#中的Bitmap类来修改图片尺寸,同时保持原有的图片比例,作者分享了自己的经验,并鼓励读者参考和使用
    2025-01-01
  • 自定义WPF分页控件的全过程记录

    自定义WPF分页控件的全过程记录

    接触WPF也有一段时间了,最近遇到了一个自定义分页控件的功能,感觉可以分享下面,所以这篇文章主要介绍了自定义WPF分页控件的相关资料,需要的朋友可以参考下
    2021-05-05
  • 解决winform界面刷新闪烁问题的多种方案

    解决winform界面刷新闪烁问题的多种方案

    这篇文章给大家介绍了如何解决winform界面刷新闪烁问题的多种方案,文中通过代码示例给大家介绍的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2024-02-02
  • c# 实现子窗口关闭父窗口也关闭的方法

    c# 实现子窗口关闭父窗口也关闭的方法

    下面小编就为大家带来一篇c# 实现子窗口关闭父窗口也关闭的方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-01-01
  • C#自动化实现高效统计Word文档字数

    C#自动化实现高效统计Word文档字数

    在日常开发或工作中,我们经常会遇到需要统计 Word 文档字数的需求,本文将利用 C# 结合强大的第三方库 Spire.Doc for .NET轻松实现 Word 文档的字数统计功能,希望对大家有所帮助
    2026-01-01

最新评论