在如今多核 CPU 普及、大数据处理需求激增的开发场景中,“单线程跑满”早已无法满足性能要求。如果你的 C# 代码还在靠普通 for 循环处理百万级数据,或是串行发送多个网络请求,那大概率会浪费 70% 以上的硬件性能。

好在 .NET 标准库自带了强大的并行处理工具,无需依赖任何第三方库,就能轻松利用多核优势提升效率。本文专为新手设计,从基础概念讲起,通过 3 个可直接复制运行的实战案例,带你掌握 Parallel.For、PLINQ 等核心工具的用法,真正搞定“数据并行”场景。

一、先搞懂 2 个核心概念:别把“并行”和“并发”搞混

入门前必须先明确一个关键区别——并行(Parallel)≠ 并发(Concurrent),这直接决定了你后续选择哪种工具解决问题。

对比维度 并行(Parallel) 并发(Concurrent)
核心逻辑 多个任务真正同时执行 多个任务交替执行(CPU 快速切换)
硬件依赖 必须多核 CPU 支持 单核 CPU 也能实现
直观比喻 多个人同时搬砖 一个人轮流搬两堆砖
.NET 工具 Parallel 类、PLINQ async/await(单线程异步)

本文聚焦“数据并行”——即把大量数据分成多个片段,用多个线程同时处理(典型场景:百万级数据转换、批量计算),用到的核心工具是 Parallel.ForPLINQ,最后再补充一个“并行请求”的实用案例(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 核心数分配线程,同时处理多个数据片段。

新手必看注意事项

  1. 索引范围问题:Parallel.For 的第二个参数是“结束索引(排他)”,比如 0 到 data.Length,实际处理的是 0 到 data.Length-1,和普通 for 循环一致,不用担心索引越界。
  2. 线程安全问题:本案例中 parallelResult[i] 是“每个线程操作独立的数组元素”,没有共享数据,所以无需加锁;如果是多个线程修改同一个变量(比如 int total = 0; total += Compute(...)),就会出现数据污染,后续案例会讲解决方案。
  3. 小任务不适合并行:如果 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 左右,且代码几乎没变化——对新手来说,这是“零学习成本”提升效率的神器。

新手必看注意事项

  1. AsParallel() 的位置:必须放在查询的开头(如 sourceData.AsParallel().Where(...)),如果放在 Where 之后(sourceData.Where(...).AsParallel()),则只有后续操作并行,筛选还是串行,效率会大打折扣。
  2. 顺序问题:PLINQ 默认不保证结果顺序(因为并行处理片段的顺序不确定),如果需要保持原顺序,可加 AsOrdered(),比如:
    var plinqOrderedResult = sourceData
        .AsParallel()
        .AsOrdered() // 保持原顺序(会增加少量开销)
        .Where(x => x % 2 == 0)
        .Select(x => Math.Sqrt(x))
        .ToList();
    
  3. 不要滥用 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 倍!

新手必看注意事项

  1. HttpClient 重用:不要在循环里新建 HttpClientnew HttpClient()),否则会导致 TCP 连接池耗尽,出现“超时”问题——本案例中只创建一个 HttpClient 复用,是正确做法。
  2. 异常处理:如果某个 API 请求失败,Task.WhenAll 会直接抛出异常,需要用 try-catch 捕获,比如:
    try
    {
        var parallelResponses = await Task.WhenAll(tasks);
    }
    catch (HttpRequestException ex)
    {
        Console.WriteLine($"API 请求失败:{ex.Message}");
    }
    
  3. 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# 并行处理的入门技能,接下来可以深入:

  • 线程安全:学习 lockInterlocked 等同步工具,解决“多线程修改共享数据”的问题;
  • 性能调优:用 ParallelOptions.MaxDegreeOfParallelism 控制并行数,避免 CPU 过载;
  • 任务控制:学习 Task.WhenAny(等待任意任务完成)、CancellationToken(取消任务)等高级用法。

最后记住:并行不是“银弹”,不是所有场景都适合并行(比如任务之间有顺序依赖,或任务耗时极短)。入门阶段,先把本文的 3 个案例练熟,再根据实际业务场景灵活选择工具,就是最高效的学习路径。

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

更多推荐