【C#避坑实战系列文章12】一文读懂 C# 并行与并发:TPL 库核心概念 + 200 行源码,新手也能懂
刚学 C# 的小伙伴常被 “并行”“并发”“异步” 绕晕:明明写了async/await,为啥程序没变快?用for循环处理数据卡半天,怎么利用多核 CPU?其实这些问题,.NET 自带的TPL 库(任务并行库) 都能解决 —— 它不用装任何第三方包,就能让你轻松玩转并行与并发,而且新手只要懂基础语法就能上手。
本文从 “掰扯清楚概念” 开始,用 3 个可直接复制的源码案例(共 200 行左右),带大家掌握 TPL 库的核心用法,最后再给新手一份 “工具选择指南”,看完就能用在实际项目里。
文章目录
一、先破后立:用 “奶茶店模型” 分清并行与并发
新手学这部分最容易犯的错:把 “并行” 和 “并发” 当一回事。其实两者的核心区别,用 “奶茶店做奶茶” 的场景就能讲明白:
- 并行(Parallel):多个人同时做
假设奶茶店有 2 个店员(对应 2 个 CPU 核心),现在来了 2 个订单:
店员 A 专门做 “珍珠奶茶”
店员 B 专门做 “杨枝甘露”
两个订单同时开始、同时完成,这就是并行。
核心特点:真正的 “同时进行”,必须依赖多核 CPU,目的是 “提高计算效率”(比如处理百万条数据)。 - 并发(Concurrent):一个人轮流做
如果奶茶店只有 1 个店员(对应 1 个 CPU 核心),来了 2 个订单:
店员先给 “珍珠奶茶” 加珍珠(做 10 秒)
接着给 “杨枝甘露” 切芒果(做 10 秒)
再回头煮珍珠奶茶的茶底(做 10 秒)
最后组装杨枝甘露(做 10 秒)
两个订单 “交替进行”,看似同时在处理,实际是店员快速切换任务,这就是并发。
核心特点:看似同时,实则交替,单核 CPU 就能实现,目的是 “提高资源利用率”(比如处理多个 API 请求,等待网络响应时不浪费时间)。 - 一句话总结 + 对比表
对比维度
并行(Parallel)
并发(Concurrent)
底层逻辑
多核 CPU 同时执行多个任务
单核 CPU 交替执行多个任务
核心工具(TPL)
Parallel 类、PLINQ
Task 类、async/await
典型场景
百万数据计算、图像处理(计算密集)
多 API 请求、文件读写(I/O 密集)
新手关键认知
追求 “快”(缩短总耗时)
追求 “不闲着”(不浪费等待时间)
二、TPL 库核心概念:3 个 “必懂组件”,新手不用记复杂原理
TPL 库(System.Threading.Tasks命名空间)是 C# 处理并行与并发的 “瑞士军刀”,核心组件就 3 个,新手不用深扒底层线程池原理,先搞懂 “怎么用、用在哪” 就行:
- Task:异步任务的 “最小单元”
你可以把Task理解成 “一个要执行的任务”—— 比如调用一个 API、计算一个数值、读一个文件。它有两种常用形式:
Task:没有返回值的任务(比如 “打印日志”)
Task:有返回值的任务(比如 “获取 API 返回的字符串”)
新手必知:Task默认是 “异步执行” 的,不会阻塞主线程(比如 UI 界面不会卡住),这是它和普通方法的最大区别。 - Parallel:数据并行的 “一键工具”
Parallel类专门用来做 “数据并行”—— 比如把 100 万条数据分成多份,让多个 CPU 核心同时处理。最常用的就是Parallel.For(并行循环),比普通for循环简单,还能自动分配 CPU 资源。
新手必知:Parallel是 “阻塞式” 的 —— 比如执行Parallel.For时,主线程会等所有并行任务做完才继续往下走,这点和Task的异步特性不同。 - 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 库 “工具选择指南” + 下一步学习
-
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(); -
新手下一步学习方向
如果能独立跑通上面 3 个案例,说明你已经掌握了 TPL 库的 “入门技能”,接下来可以针对性学习:
线程安全:深入lock语句、Monitor类,解决复杂场景下的共享数据问题;
任务控制:学习CancellationToken(取消正在执行的任务)、Task.Delay(延迟执行任务);
性能调优:用ParallelOptions.MaxDegreeOfParallelism控制并行数(避免 CPU 过载),用Task.Run指定任务优先级。
最后给新手一个小建议:刚开始不用追求 “精通所有 TPL API”,先把本文的 3 个案例用到项目里 —— 比如把原来的串行 API 调用改成Task.WhenAll,把慢的for循环改成Parallel.For,先看到实际效果,再慢慢深入原理,这样学习效率最高。
------------伴代码深耕技术、连万物探索物联,我聚焦计算机、物联网与上位机领域,盼同频的你关注,一起交流成长~
更多推荐


所有评论(0)