刚学 C# 的小伙伴常被 “并行”“并发”“异步” 绕晕:明明写了async/await,为啥程序没变快?用for循环处理数据卡半天,怎么利用多核 CPU?其实这些问题,.NET 自带的TPL 库(任务并行库) 都能解决 —— 它不用装任何第三方包,就能让你轻松玩转并行与并发,而且新手只要懂基础语法就能上手。
本文从 “掰扯清楚概念” 开始,用 3 个可直接复制的源码案例(共 200 行左右),带大家掌握 TPL 库的核心用法,最后再给新手一份 “工具选择指南”,看完就能用在实际项目里。

一、先破后立:用 “奶茶店模型” 分清并行与并发

新手学这部分最容易犯的错:把 “并行” 和 “并发” 当一回事。其实两者的核心区别,用 “奶茶店做奶茶” 的场景就能讲明白:

  1. 并行(Parallel):多个人同时做
    假设奶茶店有 2 个店员(对应 2 个 CPU 核心),现在来了 2 个订单:
    店员 A 专门做 “珍珠奶茶”
    店员 B 专门做 “杨枝甘露”
    两个订单同时开始、同时完成,这就是并行。
    核心特点:真正的 “同时进行”,必须依赖多核 CPU,目的是 “提高计算效率”(比如处理百万条数据)。
  2. 并发(Concurrent):一个人轮流做
    如果奶茶店只有 1 个店员(对应 1 个 CPU 核心),来了 2 个订单:
    店员先给 “珍珠奶茶” 加珍珠(做 10 秒)
    接着给 “杨枝甘露” 切芒果(做 10 秒)
    再回头煮珍珠奶茶的茶底(做 10 秒)
    最后组装杨枝甘露(做 10 秒)
    两个订单 “交替进行”,看似同时在处理,实际是店员快速切换任务,这就是并发。
    核心特点:看似同时,实则交替,单核 CPU 就能实现,目的是 “提高资源利用率”(比如处理多个 API 请求,等待网络响应时不浪费时间)。
  3. 一句话总结 + 对比表
    对比维度
    并行(Parallel)
    并发(Concurrent)
    底层逻辑
    多核 CPU 同时执行多个任务
    单核 CPU 交替执行多个任务
    核心工具(TPL)
    Parallel 类、PLINQ
    Task 类、async/await
    典型场景
    百万数据计算、图像处理(计算密集)
    多 API 请求、文件读写(I/O 密集)
    新手关键认知
    追求 “快”(缩短总耗时)
    追求 “不闲着”(不浪费等待时间)

二、TPL 库核心概念:3 个 “必懂组件”,新手不用记复杂原理

TPL 库(System.Threading.Tasks命名空间)是 C# 处理并行与并发的 “瑞士军刀”,核心组件就 3 个,新手不用深扒底层线程池原理,先搞懂 “怎么用、用在哪” 就行:

  1. Task:异步任务的 “最小单元”
    你可以把Task理解成 “一个要执行的任务”—— 比如调用一个 API、计算一个数值、读一个文件。它有两种常用形式:
    Task:没有返回值的任务(比如 “打印日志”)
    Task:有返回值的任务(比如 “获取 API 返回的字符串”)
    新手必知:Task默认是 “异步执行” 的,不会阻塞主线程(比如 UI 界面不会卡住),这是它和普通方法的最大区别。
  2. Parallel:数据并行的 “一键工具”
    Parallel类专门用来做 “数据并行”—— 比如把 100 万条数据分成多份,让多个 CPU 核心同时处理。最常用的就是Parallel.For(并行循环),比普通for循环简单,还能自动分配 CPU 资源。
    新手必知:Parallel是 “阻塞式” 的 —— 比如执行Parallel.For时,主线程会等所有并行任务做完才继续往下走,这点和Task的异步特性不同。
  3. PLINQ:并行查询的 “语法糖”
    PLINQ 是 “并行版 LINQ”,只要在普通 LINQ 查询前加个AsParallel(),就能让查询并行执行。比如从 100 万条数据里筛选偶数,PLINQ 会自动分批次处理,比普通 LINQ 快很多。
    新手必知:PLINQ 的语法和普通 LINQ 几乎一样,新手不用学新语法,加个方法就能提速,性价比极高。

三、200 行源码实战:3 个案例覆盖 TPL 核心用法

下面 3 个案例,全部基于.NET 5+(Visual Studio 2022 就能跑),代码带详细注释,新手复制到项目里就能运行,还能直观看到效果。
案例 1:用 Task 实现 “并发任务”(处理 I/O 密集场景)
场景:模拟调用 2 个 API(I/O 密集型任务,主要耗时在 “等网络响应”),用Task实现并发,对比串行执行的耗时。

using System;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;

namespace TplDemo
{
    class TaskDemo
    {
        // 新手注意:C# 7.1+支持async Main,版本不够就改成void Main,内部用Wait()
        static async Task Main(string[] args)
        {
            // 1. 准备2个模拟API地址(实际项目换自己的接口)
            string api1 = "https://jsonplaceholder.typicode.com/todos/1";
            string api2 = "https://jsonplaceholder.typicode.com/todos/2";
            var httpClient = new HttpClient(); // 复用HttpClient,避免连接池耗尽
            // 2. 串行执行:先调API1,再调API2(耗时=API1耗时+API2耗时)
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            // 串行调用:等API1返回再调API2
            string result1 = await httpClient.GetStringAsync(api1);
            string result2 = await httpClient.GetStringAsync(api2);
            stopwatch.Stop();
            Console.WriteLine($"串行执行耗时:{stopwatch.ElapsedMilliseconds} 毫秒");
            Console.WriteLine($"API1返回长度:{result1.Length} 字节\n");

            // 3. 并发执行:用Task同时调2个API(耗时≈较慢的那个API的耗时)
            stopwatch.Restart();
            // 第一步:创建2个Task(此时任务还没执行,只是“准备好”)
            Task<string> task1 = httpClient.GetStringAsync(api1);
            Task<string> task2 = httpClient.GetStringAsync(api2);
            // 第二步:等待所有Task完成(并发执行,不阻塞主线程)
            await Task.WhenAll(task1, task2);
            // 第三步:获取结果(Task.Result也能拿,但await更安全,不会卡主线程)
            string concurrentResult1 = task1.Result;
            string concurrentResult2 = task2.Result;
            stopwatch.Stop();
            Console.WriteLine($"并发执行耗时:{stopwatch.ElapsedMilliseconds} 毫秒");
            Console.WriteLine($"API2返回长度:{concurrentResult2.Length} 字节");
        }
    }
 }

运行结果(参考):
串行执行耗时:850 毫秒
API1返回长度:292 字节

并发执行耗时:420 毫秒
API2返回长度:294 字节
新手注意事项:
不要在循环里新建HttpClient(比如new HttpClient()),会导致 TCP 连接池耗尽,复用一个就行;
Task.WhenAll会等待所有任务完成,如果要 “只要有一个任务完成就继续”,用Task.WhenAny(比如多个 API 选最快的那个返回)。

案例 2:用 Parallel 实现 “并行计算”(处理计算密集场景)
场景:计算 100 万条整数的 “平方和”(计算密集型任务,主要耗时在 CPU 计算),对比普通for循环和Parallel.For的耗时。

using System;
using System.Diagnostics;
using System.Threading.Tasks;

namespace TplDemo
{
    class ParallelDemo
    {
        static void Main(string[] args)
        {
            // 1. 准备100万条数据(0到999999)
            int dataCount = 1000000;
            int[] data = new int[dataCount];
            for (int i = 0; i < dataCount; i++)
            {
                data[i] = i;
            }

            // 2. 普通for循环:串行计算平方和(单CPU核心工作)
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            long normalSum = 0;
            for (int i = 0; i < dataCount; i++)
            {
                normalSum += (long)Math.Pow(data[i], 2); // 计算平方并累加
            }
            stopwatch.Stop();
            Console.WriteLine($"普通for循环耗时:{stopwatch.ElapsedMilliseconds} 毫秒");
            Console.WriteLine($"串行计算结果:{normalSum}\n");

            // 3. Parallel.For:并行计算平方和(多CPU核心同时工作)
            stopwatch.Restart();
            long parallelSum = 0;
            // Parallel.For(起始索引, 结束索引(排他), 循环体)
            Parallel.For(0, dataCount, i =>
            {
                // 关键:多线程修改共享变量parallelSum,必须用Interlocked保证线程安全
                Interlocked.Add(ref parallelSum, (long)Math.Pow(data[i], 2));
            });
            stopwatch.Stop();
            Console.WriteLine($"Parallel.For耗时:{stopwatch.ElapsedMilliseconds} 毫秒");
            Console.WriteLine($"并行计算结果:{parallelSum}");
        }
    }
}

运行结果(参考,4 核 CPU):
普通for循环耗时:180 毫秒
串行计算结果:333332833333500000

Parallel.For耗时:55 毫秒
并行计算结果:333332833333500000

新手注意事项:
线程安全:多线程修改同一个变量(比如parallelSum)时,必须用Interlocked类(或lock),否则会出现 “数据污染”(结果不对);
小任务不适合并行:如果计算逻辑很简单(比如data[i] + 1),Parallel 可能比普通 for 慢 —— 因为线程切换的开销超过了并行收益。

案例 3:用 PLINQ 实现 “并行查询”(处理数据筛选场景)
场景:从 100 万条整数中,筛选出 “能被 3 整除且大于 1000” 的数,再计算它们的平方根,对比普通 LINQ 和 PLINQ 的耗时。

using System;
using System.Diagnostics;
using System.Linq;

namespace TplDemo
{
    class PlinqDemo
    {
        static void Main(string[] args)
        {
            // 1. 生成1到100万的整数集合
            var data = Enumerable.Range(1, 1000000);

            // 2. 普通LINQ:串行查询
            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            var normalQuery = data
                .Where(x => x % 3 == 0 && x > 1000) // 筛选条件
                .Select(x => Math.Sqrt(x)) // 计算平方根
                .ToList(); // 触发查询(LINQ默认延迟执行)
            stopwatch.Stop();
            Console.WriteLine($"普通LINQ耗时:{stopwatch.ElapsedMilliseconds} 毫秒");
            Console.WriteLine($"筛选结果数量:{normalQuery.Count}\n");

            // 3. PLINQ:并行查询(仅加AsParallel())
            stopwatch.Restart();
            var plinqQuery = data
                .AsParallel() // 关键:启用并行查询
                .Where(x => x % 3 == 0 && x > 1000)
                .Select(x => Math.Sqrt(x))
                .ToList(); // 等待并行任务完成,合并结果
            stopwatch.Stop();
            Console.WriteLine($"PLINQ耗时:{stopwatch.ElapsedMilliseconds} 毫秒");
            Console.WriteLine($"筛选结果数量:{plinqQuery.Count}");
        }
    }
}

运行结果(参考):
普通LINQ耗时:120 毫秒
筛选结果数量:332666

PLINQ耗时:35 毫秒
筛选结果数量:332666

新手注意事项:
AsParallel()要放在查询开头:如果放在Where之后,只有后续操作并行,筛选还是串行,效率会低很多;
保持顺序用AsOrdered():PLINQ 默认不保证结果顺序(并行处理片段顺序不确定),如果需要和原数据顺序一致,加AsParallel().AsOrdered()(会多一点开销)。

四、新手总结:TPL 库 “工具选择指南” + 下一步学习

  1. 30 秒选对工具(新手直接对号入座)
    实际场景
    选哪个 TPL 组件?
    代码关键片段
    多 API 请求、文件读写(I/O 密集)
    Task + Task.WhenAll
    var task1 = http.GetAsync(url); await Task.WhenAll(task1);
    百万数据循环计算(计算密集)
    Parallel.For
    Parallel.For(0, count, i => { 计算逻辑; });
    数据筛选 + 转换(查询场景)
    PLINQ
    data.AsParallel().Where(…).Select(…).ToList();

  2. 新手下一步学习方向
    如果能独立跑通上面 3 个案例,说明你已经掌握了 TPL 库的 “入门技能”,接下来可以针对性学习:
    线程安全:深入lock语句、Monitor类,解决复杂场景下的共享数据问题;
    任务控制:学习CancellationToken(取消正在执行的任务)、Task.Delay(延迟执行任务);
    性能调优:用ParallelOptions.MaxDegreeOfParallelism控制并行数(避免 CPU 过载),用Task.Run指定任务优先级。
    最后给新手一个小建议:刚开始不用追求 “精通所有 TPL API”,先把本文的 3 个案例用到项目里 —— 比如把原来的串行 API 调用改成Task.WhenAll,把慢的for循环改成Parallel.For,先看到实际效果,再慢慢深入原理,这样学习效率最高。

------------伴代码深耕技术、连万物探索物联,我聚焦计算机、物联网与上位机领域,盼同频的你关注,一起交流成长~

更多推荐