C# 的任务并行库(TPL)

C#任务并行库(TPL)

C# 的任务并行库(Task Parallel Library, TPL)是 System.Threading.Tasks 命名空间下的一组 API,旨在简化并行编程,让开发者能更轻松地利用多核处理器的性能,而无需直接管理底层线程的创建、调度和销毁

类别 方法/类 签名示例 适用场景 关键特点
‌并行循环‌ Parallel.For Parallel.For(0, 100, i => { ... }) 已知迭代次数的数值循环 替代传统 for,自动并行化迭代。注意索引 i 的顺序不保证。
‌并行循环‌ Parallel.ForEach Parallel.ForEach(list, item => { ... }) 遍历集合(List, Array等) 替代 foreach,对集合元素并行处理。适合元素间无依赖的操作。
‌并行 LINQ‌ AsParallel() source.AsParallel().Where(...).Sum() 数据查询与聚合计算 PLINQ,将 LINQ 查询并行化。适合 CPU 密集型的数据过滤、映射、归约。
‌任务启动‌ Task.Run Task.Run(() => { ... }) 将同步代码卸载到后台线程 最常用的异步/并行入口。将工作排队到线程池,返回 Task。
‌任务组合‌ Task.WhenAll await Task.WhenAll(task1, task2) 等待多个任务全部完成 并行执行多个独立任务,所有完成后继续。返回结果数组。
‌任务组合‌ Task.WhenAny await Task.WhenAny(task1, task2) 竞速模式/超时控制 只要有一个任务完成就返回。常用于冗余请求或超时fallback。
‌任务工厂‌ TaskFactory new TaskFactory().StartNew(...) 高级任务创建与控制 比 Task.Run 更底层,可指定 TaskCreationOptions(如 LongRunning)。
‌取消支持‌ CancellationToken Parallel.For(..., options, body) 需要中途停止并行操作 配合 ParallelOptions 或 Task 使用,实现优雅的中断机制。
‌线程局部存储‌ ThreadLocal new ThreadLocal(() => 0) 并行循环中的累加/状态保持 避免锁竞争。每个线程拥有独立的变量副本,最后合并结果。

注意事项

关注点 详细说明 建议做法
任务类型区分 CPU密集型 vs I/O密集型 是选择并行方案的首要前提。 CPU密集型:用 Parallel.For/ForEach、PLINQ、Task.Run。 I/O密集型(网络、数据库、文件):用 async/await + Task.WhenAll
线程安全 并行循环中的多个线程可能同时访问共享资源,导致竞态条件。 使用线程安全的并发集合,如 ConcurrentBag<T>ConcurrentDictionary<K,V>。避免在循环内直接操作普通 List<T>
异常处理 并行任务中的异常会被包装,不会立即抛出。 捕获 AggregateException 并遍历其 InnerExceptions 属性来处理所有异常。
过度并行化 并非任务越多越快,过多的并行会导致上下文切换开销剧增,反而降低性能。 TPL 会自动根据 CPU 核心数调整并行度。对于 I/O 任务,可使用 SemaphoreSlim 进行限流。
取消操作 长时间运行的并行任务需要支持优雅取消。 使用 CancellationToken,并在循环体或任务内部定期检查 token.IsCancellationRequested
结果顺序 Parallel.ForEach 和 PLINQ 默认不保证执行顺序。 如果业务依赖顺序,PLINQ 可使用 .AsOrdered(),但会有性能损耗;Parallel.ForEach 则需预分配数组按索引写入。

使用举例

1、并行循环 (Parallel Loops)

适用于‌CPU 密集型‌且‌迭代之间无依赖‌的场景

c# 复制代码
static async Task Main(string[] args)
{
    // 1. Parallel.For: 处理大量数值计算
    Parallel.For(10000000, 10000010, i =>
    {
        // 注意:这里没有顺序保证
        CalculatePi(i);
        Console.WriteLine("For done threadId={0}, i={1}", Thread.CurrentThread.ManagedThreadId, i);
    });

    var items = new List<int> { 10000000, 20000000, 40000000, 90000000 };
    // 2. Parallel.ForEach: 处理集合
    Parallel.ForEach(items, i =>
    {
        CalculatePi(i);
        Console.WriteLine("ForEach done threadId={0}, i={1}", Thread.CurrentThread.ManagedThreadId, i);
    });


    // 3. 带取消支持和最大并行度的配置
    CancellationTokenSource cts = new CancellationTokenSource(100);
    CancellationToken token = cts.Token;
    var options = new ParallelOptions
    {
        MaxDegreeOfParallelism = 2, // 限制并发线程数
        CancellationToken = token
    };

    try
    {
        Parallel.For(400000000, 400000010, options, i =>
        {
            // 检查取消信号
            options.CancellationToken.ThrowIfCancellationRequested();
            // if (!token.IsCancellationRequested)
            {
                CalculatePi(i);
                Console.WriteLine("For cancel done threadId={0}, i={1}", Thread.CurrentThread.ManagedThreadId, i);
            }
        });
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("操作已取消");
    }
}

2、并行 LINQ (PLINQ)

适用于‌数据流处理‌,语法简洁,但要注意无序性

c# 复制代码
static async Task Main(string[] args)
{
    // 并行查询:筛选偶数并求平方和
    // AsParallel() 开启并行
    // AsOrdered() 保持顺序(有性能开销)
    var result = Enumerable.Range(1, 1000).AsParallel()
                        .WithDegreeOfParallelism(2) // 可选:限制并行度
                        .Where(n => n % 2 == 0)
                        .Select(n => n * n)
                        .Sum();
    Console.WriteLine("done");
}

3、任务组合

适用于‌异步 I/O‌ 或 ‌混合并行‌ 场景

c# 复制代码
static async Task Main(string[] args)
{
    HttpClient client = new HttpClient();
    // 同时发起三个 HTTP 请求
    var task1 = client.GetAsync("https://www.baidu.com");
    var task2 = client.GetAsync("https://www.bing.com");
    var task3 = client.GetAsync("https://www.2345.com");

    // 等待所有请求完成
    var responses = await Task.WhenAll(task1, task2, task3);
    // var response = Task.WhenAny(task1, task2, task3);

    // 处理结果
    foreach (var response in responses)
    {
        Console.WriteLine(response.StatusCode);
    }
}

4、Task.Run与Task.Factory.StartNew

特性 Task.Run (.NET 4.5+ 引入) Task.Factory.StartNew (.NET 4.0 引入)
默认调度器 始终使用线程池 (TaskScheduler.Default),安全地将工作卸载到后台。 继承当前上下文调度器。若在 UI 线程调用,可能会意外在 UI 线程执行,导致界面卡死。
返回值处理 自动解包。若传入 async Lambda,返回 Task<T> 而非 Task<Task<T>> 不自动解包。若传入 async Lambda,返回嵌套的 Task<Task>,需额外等待内层任务。
子任务附加 默认使用 DenyChildAttach,禁止子任务附加到当前父任务实例。 允许子任务通过 AttachedToParent 选项附加为父任务的子任务。
适用场景 90% 以上的常规后台任务(简单直接)。 需要高度自定义的高级场景(如指定自定义调度器、长耗时任务等)。
c# 复制代码
static async Task Main(string[] args)
{
   // ✅ 使用 Task.Run:自动解包,直接返回 Task
   Task task1 = Task.Run(async () =>
   {
       await Task.Delay(100); // 异步操作
       Console.WriteLine("Task.Run 完成");
   });
   await task1; // 正确等待内部异步操作完全执行完毕


   // ❌ 使用 StartNew:不自动解包,返回 Task<Task>
   Task<Task> task2 = Task.Factory.StartNew(async () =>
   {
       await Task.Delay(100);
       Console.WriteLine("StartNew 完成");
   });
   await task2; // 仅等待外层任务完成!内层异步操作可能尚未执行完!
   await task2.Result; // 必须这样写才能正确等待内层任务
}

使用建议

  • 默认首选 Task.Run:如果你只是想把一段代码扔到后台执行,或者需要保持 UI 响应性,直接使用 Task.Run(() => {...})。它一行代码就能搞定,且避免了上下文切换和嵌套任务的陷阱
  • 仅当你需要精细控制任务配置时才使用它。例如:
    • 使用 TaskCreationOptions.LongRunning 标记一个极度耗时的任务,让调度器为其分配专属线程,避免长时间占用线程池资源。
    • 需要指定特定的 TaskScheduler(如强制将任务调度回 UI 线程更新控件)
    • 需要传递特定的 CancellationToken 或组合复杂的 TaskCreationOptions
c# 复制代码
// 如果你想用 StartNew 达到与 Task.Run 完全相同的默认安全行为,你需要显式指定四个参数
Task.Factory.StartNew(() => 
{
    // TODO
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
相关推荐
快乐的哈士奇2 小时前
【Next.js实战①】Gmail API 按柜号检索邮件:OAuth 双 Cookie 与搜索 Fallback
开发语言·javascript·ecmascript
weixin_307779132 小时前
Python写入Shell文件使用Linux系统的换行符
linux·开发语言·python·自动化
zmzb01032 小时前
Python课后习题训练记录Day130
开发语言·python
阿里嘎多学长2 小时前
2026-06-13 GitHub 热点项目精选
开发语言·程序员·github·代码托管
xiaoshuaishuai82 小时前
C# 委托与事件
开发语言·c#
kmblack12 小时前
javascript计算年龄
开发语言·javascript·ecmascript
肖爱Kun3 小时前
STL标准模块库操作
开发语言·音视频
Song_da_da_3 小时前
C# 接口(Interface)深度解析:规范、解耦与灵活扩展
开发语言·c#