【C#避坑实战系列文章13】C# 并行处理入门:从 Parallel.For 到 PLINQ,3 个实战案例搞定数据并行(保姆级教程)
在如今多核 CPU 普及、大数据处理需求激增的开发场景中,“单线程跑满”早已无法满足性能要求。如果你的 C# 代码还在靠普通 for 循环处理百万级数据,或是串行发送多个网络请求,那大概率会浪费 70% 以上的硬件性能。
好在 .NET 标准库自带了强大的并行处理工具,无需依赖任何第三方库,就能轻松利用多核优势提升效率。本文专为新手设计,从基础概念讲起,通过 3 个可直接复制运行的实战案例,带你掌握 Parallel.For、PLINQ 等核心工具的用法,真正搞定“数据并行”场景。
文章目录
一、先搞懂 2 个核心概念:别把“并行”和“并发”搞混
入门前必须先明确一个关键区别——并行(Parallel)≠ 并发(Concurrent),这直接决定了你后续选择哪种工具解决问题。
| 对比维度 | 并行(Parallel) | 并发(Concurrent) |
|---|---|---|
| 核心逻辑 | 多个任务真正同时执行 | 多个任务交替执行(CPU 快速切换) |
| 硬件依赖 | 必须多核 CPU 支持 | 单核 CPU 也能实现 |
| 直观比喻 | 多个人同时搬砖 | 一个人轮流搬两堆砖 |
| .NET 工具 | Parallel 类、PLINQ | async/await(单线程异步) |
本文聚焦“数据并行”——即把大量数据分成多个片段,用多个线程同时处理(典型场景:百万级数据转换、批量计算),用到的核心工具是 Parallel.For 和 PLINQ,最后再补充一个“并行请求”的实用案例(Task.WhenAll)。
二、实战案例 1:用 Parallel.For 处理百万级数据计算
场景描述
假设有 100 万条整数数据,需要对每条数据执行“开平方 + 开四次方”的计算(模拟计算密集型任务),用普通 for 循环和 Parallel.For 对比,看效率差异。
步骤 1:准备代码(可直接复制运行)
using System;
using System.Diagnostics; // 用于计时
using System.Threading.Tasks;
class ParallelForDemo
{
static void Main()
{
// 1. 初始化 100 万条数据(0 到 999999)
int[] data = new int[1000000];
for (int i = 0; i < data.Length; i++)
{
data[i] = i;
}
// 2. 用普通 for 循环处理(作为对比)
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start(); // 开始计时
int[] normalResult = new int[data.Length];
for (int i = 0; i < data.Length; i++)
{
normalResult[i] = Compute(data[i]);
}
stopwatch.Stop(); // 结束计时
Console.WriteLine($"普通 for 循环耗时:{stopwatch.ElapsedMilliseconds} 毫秒");
// 3. 用 Parallel.For 并行处理
stopwatch.Restart(); // 重置计时器
int[] parallelResult = new int[data.Length];
// Parallel.For(起始索引, 结束索引(排他), 循环体委托)
Parallel.For(0, data.Length, i =>
{
parallelResult[i] = Compute(data[i]);
});
stopwatch.Stop();
Console.WriteLine($"Parallel.For 并行耗时:{stopwatch.ElapsedMilliseconds} 毫秒");
Console.WriteLine("处理完成!(可对比两组结果长度,验证正确性)");
Console.WriteLine($"普通 for 结果长度:{normalResult.Length}");
Console.WriteLine($"Parallel.For 结果长度:{parallelResult.Length}");
}
// 模拟计算密集型操作:开平方(Math.Sqrt) + 开四次方(Math.Pow(value, 0.25))
static int Compute(int value)
{
// 注意:value 为 0 时,0 的任意次方都是 0,无需特殊处理
return (int)(Math.Sqrt(value) * Math.Pow(value, 0.25));
}
}
步骤 2:运行结果分析(以 4 核 CPU 为例)
普通 for 循环耗时:186 毫秒
Parallel.For 并行耗时:52 毫秒
处理完成!
普通 for 结果长度:1000000
Parallel.For 结果长度:1000000
可以看到:并行处理耗时仅为普通循环的 1/3 左右,这就是多核 CPU 的优势——Parallel.For 会自动根据 CPU 核心数分配线程,同时处理多个数据片段。
新手必看注意事项
- 索引范围问题:Parallel.For 的第二个参数是“结束索引(排他)”,比如
0 到 data.Length,实际处理的是0 到 data.Length-1,和普通 for 循环一致,不用担心索引越界。 - 线程安全问题:本案例中
parallelResult[i]是“每个线程操作独立的数组元素”,没有共享数据,所以无需加锁;如果是多个线程修改同一个变量(比如int total = 0; total += Compute(...)),就会出现数据污染,后续案例会讲解决方案。 - 小任务不适合并行:如果
Compute方法只是简单返回value + 1(耗时极短),Parallel.For 可能比普通循环慢——因为“线程创建/切换”的开销超过了并行带来的收益。
三、实战案例 2:用 PLINQ 实现并行查询(筛选+计算)
场景描述
PLINQ 是“并行版 LINQ”,语法和普通 LINQ 几乎一致,只需加一个 AsParallel() 方法,就能让查询并行执行。适合“筛选+转换”类场景,比如从 100 万条整数中筛选出偶数,再计算每个偶数的平方根。
步骤 1:准备代码(可直接复制运行)
using System;
using System.Diagnostics;
using System.Linq; // PLINQ 依赖 Linq 命名空间
class PlinqDemo
{
static void Main()
{
// 1. 生成 1 到 1000000 的整数集合
var sourceData = Enumerable.Range(1, 1000000);
// 2. 普通 LINQ 查询(作为对比)
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
var normalLinqResult = sourceData
.Where(x => x % 2 == 0) // 筛选偶数
.Select(x => Math.Sqrt(x)) // 计算平方根
.ToList(); // 转为列表(触发查询执行)
stopwatch.Stop();
Console.WriteLine($"普通 LINQ 耗时:{stopwatch.ElapsedMilliseconds} 毫秒");
Console.WriteLine($"普通 LINQ 找到偶数数量:{normalLinqResult.Count}");
// 3. PLINQ 并行查询(仅多了 AsParallel())
stopwatch.Restart();
var plinqResult = sourceData
.AsParallel() // 关键:启用并行查询
.Where(x => x % 2 == 0) // 并行筛选
.Select(x => Math.Sqrt(x)) // 并行计算
.ToList(); // 等待所有并行任务完成,合并结果
stopwatch.Stop();
Console.WriteLine($"PLINQ 并行耗时:{stopwatch.ElapsedMilliseconds} 毫秒");
Console.WriteLine($"PLINQ 找到偶数数量:{plinqResult.Count}");
}
}
步骤 2:运行结果分析
普通 LINQ 耗时:128 毫秒
普通 LINQ 找到偶数数量:500000
PLINQ 并行耗时:35 毫秒
PLINQ 找到偶数数量:500000
结论:PLINQ 耗时仅为普通 LINQ 的 1/3 左右,且代码几乎没变化——对新手来说,这是“零学习成本”提升效率的神器。
新手必看注意事项
- AsParallel() 的位置:必须放在查询的开头(如
sourceData.AsParallel().Where(...)),如果放在Where之后(sourceData.Where(...).AsParallel()),则只有后续操作并行,筛选还是串行,效率会大打折扣。 - 顺序问题:PLINQ 默认不保证结果顺序(因为并行处理片段的顺序不确定),如果需要保持原顺序,可加
AsOrdered(),比如:var plinqOrderedResult = sourceData .AsParallel() .AsOrdered() // 保持原顺序(会增加少量开销) .Where(x => x % 2 == 0) .Select(x => Math.Sqrt(x)) .ToList(); - 不要滥用 PLINQ:如果数据量很小(比如 100 条),PLINQ 可能比普通 LINQ 慢,因为并行的“ overhead ”(开销)大于收益。
四、实战案例 3:用 Task.WhenAll 实现并行网络请求
场景描述
除了“数据并行”,“多任务并行”也很常见,比如同时调用 3 个 API 接口获取数据(I/O 密集型任务)。用 Task.WhenAll 可以等待所有任务同时完成,比串行调用快很多。
步骤 1:准备代码(可直接复制运行)
using System;
using System.Diagnostics;
using System.Net.Http; // 依赖 HttpClient 命名空间
using System.Threading.Tasks;
using System.Linq;
class TaskWhenAllDemo
{
// 注意:C# 7.1+ 才支持 async Main,若版本低,可改为 void Main,内部用 Wait()
static async Task Main()
{
// 1. 准备 3 个 API 地址(这里用 example.com 测试,实际可替换为业务接口)
var apiUrls = new[]
{
"https://example.com/api/data1",
"https://example.com/api/data2",
"https://example.com/api/data3"
};
// 2. 串行调用 API(作为对比)
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
var httpClient = new HttpClient();
foreach (var url in apiUrls)
{
var response = await httpClient.GetStringAsync(url);
Console.WriteLine($"串行请求 {url}:数据长度 {response.Length} 字节");
}
stopwatch.Stop();
Console.WriteLine($"串行请求总耗时:{stopwatch.ElapsedMilliseconds} 毫秒\n");
// 3. 用 Task.WhenAll 并行调用 API
stopwatch.Restart();
// 先创建所有任务(此时任务未执行,只是“准备状态”)
var tasks = apiUrls.Select(url =>
{
// 每个 URL 对应一个“获取数据”的任务
return httpClient.GetStringAsync(url);
});
// 关键:等待所有任务同时完成,返回结果数组(顺序和 apiUrls 一致)
var parallelResponses = await Task.WhenAll(tasks);
stopwatch.Stop();
// 遍历结果(结果顺序和 apiUrls 一一对应)
for (int i = 0; i < apiUrls.Length; i++)
{
Console.WriteLine($"并行请求 {apiUrls[i]}:数据长度 {parallelResponses[i].Length} 字节");
}
Console.WriteLine($"并行请求总耗时:{stopwatch.ElapsedMilliseconds} 毫秒");
}
}
步骤 2:运行结果分析(假设每个 API 响应时间约 500 毫秒)
串行请求 https://example.com/api/data1:数据长度 1256 字节
串行请求 https://example.com/api/data2:数据长度 1302 字节
串行请求 https://example.com/api/data3:数据长度 1288 字节
串行请求总耗时:1580 毫秒
并行请求 https://example.com/api/data1:数据长度 1256 字节
并行请求 https://example.com/api/data2:数据长度 1302 字节
并行请求 https://example.com/api/data3:数据长度 1288 字节
并行请求总耗时:520 毫秒
结论:并行请求总耗时约等于单个 API 的响应时间(520 毫秒),而串行耗时是 3 个 API 之和(1580 毫秒),效率提升 3 倍!
新手必看注意事项
- HttpClient 重用:不要在循环里新建
HttpClient(new HttpClient()),否则会导致 TCP 连接池耗尽,出现“超时”问题——本案例中只创建一个HttpClient复用,是正确做法。 - 异常处理:如果某个 API 请求失败,
Task.WhenAll会直接抛出异常,需要用try-catch捕获,比如:try { var parallelResponses = await Task.WhenAll(tasks); } catch (HttpRequestException ex) { Console.WriteLine($"API 请求失败:{ex.Message}"); } - async Main 版本问题:如果你的 Visual Studio 提示“不支持 async Main”,可右键项目 → 属性 → 生成 → 高级 → 语言版本,选择 C# 7.1 或更高。
五、入门总结:3 个工具的适用场景与下一步学习方向
1. 工具选择指南(新手直接对号入座)
| 工具 | 适用场景 | 核心优势 | 注意事项 |
|---|---|---|---|
| Parallel.For | 百万级数据循环处理(计算密集型) | 自动分配线程,无需管并发数 | 避免共享数据,防线程安全问题 |
| PLINQ | 数据筛选+转换(查询类场景) | 语法和 LINQ 一致,易上手 | 小数据量不用,可加 AsOrdered() 保顺序 |
| Task.WhenAll | 多 API/IO 任务并行(I/O 密集型) | 等待所有任务完成,结果有序 | 重用 HttpClient,处理异常 |
2. 下一步学习方向
如果你已经能独立运行上述 3 个案例,说明已经掌握了 C# 并行处理的入门技能,接下来可以深入:
- 线程安全:学习
lock、Interlocked等同步工具,解决“多线程修改共享数据”的问题; - 性能调优:用
ParallelOptions.MaxDegreeOfParallelism控制并行数,避免 CPU 过载; - 任务控制:学习
Task.WhenAny(等待任意任务完成)、CancellationToken(取消任务)等高级用法。
最后记住:并行不是“银弹”,不是所有场景都适合并行(比如任务之间有顺序依赖,或任务耗时极短)。入门阶段,先把本文的 3 个案例练熟,再根据实际业务场景灵活选择工具,就是最高效的学习路径。
------------伴代码深耕技术、连万物探索物联,我聚焦计算机、物联网与上位机领域,盼同频的你关注,一起交流成长~
更多推荐

所有评论(0)