首页 / C# 入门教程 / Task.Run 与并行

C# 入门教程

Task.Run 与并行

本教程共 100 篇 · 第 85 篇 · 更新于 2026-07-31 · 约 7 分钟阅读

C#C# 入门教程编程语言并行ParallelPLINQ

85. Task.Run 与并行

本节目标:学完能区分并行与并发,会用 Task.Run、Parallel.For 与 PLINQ 做 CPU 并行计算。

并行与异步不是一回事

第 81 章说过:异步是一个人边等边干,目的是别浪费等待时的线程。并行是多个人同时算,目的是把一堆计算尽快算完。

并行针对 CPU 密集型任务。如果你有八核 CPU,却只用一核串行算,就是浪费。把活拆开分给多核,总时间能显著缩短。

Note

并行靠多线程/多核同时执行;异步靠让出线程等 IO。两者可以结合,但动机不同。

Task.Run 启动并行任务

Task.Run 把方法丢进线程池。想并行处理多项独立工作,就启动多个 Task,再一起等它们完成。

Task t1 = Task.Run(() => Compute(1));
Task t2 = Task.Run(() => Compute(2));
Task t3 = Task.Run(() => Compute(3));

await Task.WhenAll(t1, t2, t3);
Console.WriteLine("三份计算都完成");

void Compute(int id)
{
    long sum = 0;
    for (int i = 0; i < 5_000_000; i++) sum += i;
    Console.WriteLine($"任务 {id} 算完:{sum}");
}
Tip

任务彼此独立、没有共享可变状态,并行最安全。若要共享变量,得加锁,反而可能抵消收益。

Parallel.For:为循环并行而生

Parallel.For 专门把循环体并行化,替你管理线程分工,比手搓 Task 更省心。

int[] results = new int[10];
Parallel.For(0, 10, i =>
{
    long sum = 0;
    for (int k = 0; k < 1_000_000; k++) sum += k;
    results[i] = (int)(sum % 100);
});

Console.WriteLine($"并行算完,results[0]={results[0]}");

注意:循环体里不要写同一个共享变量而不加保护,否则结果不确定。

Warning

Parallel.For 的循环体必须彼此独立。若它们竞争同一份数据,要加锁或改用局部累加,否则会出竞态(race condition)。

PLINQ:并行的 LINQ

PLINQ 给 LINQ 加个 AsParallel(),让查询在多核上分段执行,最后合并结果。

int[] numbers = [1, 2, 3, 4, 5, 6, 7, 8];
int sum = numbers
    .AsParallel()
    .Where(n => n % 2 == 0)
    .Select(n => n * n)
    .Sum();

Console.WriteLine($"偶数平方和:{sum}");

适合”数据量大、每项处理独立”的查询。数据量小反而因调度开销更慢。

何时用并行

满足以下条件再考虑并行:

  1. 任务是 CPU 密集型(大量计算)。
  2. 各项工作彼此独立,或可安全合并结果。
  3. 数据量足够大,能盖过线程调度开销。

不满足就别并行——串行更清晰,也不容易出 bug。

Tip

别对 IO 操作上 Parallel。读十个网页该用并发异步(多 Task + await),而非 Parallel 占满线程。

并行也有天花板

核心数有限,任务拆太碎,线程切换开销会超过收益。不是核越多越快,也不是任务越碎越好。

普通笔记本四到八核,超过这个并发度的纯计算,边际收益迅速下降。

Note

Environment.ProcessorCount 可查逻辑核心数,作为并行度的参考上限。

控制并行度:ParallelOptions

并行不等于”能开多少开多少”。用 ParallelOptionsMaxDegreeOfParallelism 可限制同时跑几个,避免把机器压满。

ParallelOptions options = new() { MaxDegreeOfParallelism = 4 };
Parallel.For(0, 100, options, i =>
{
    // 最多 4 个并发,其余排队
    Compute(i);
});

在共享资源有限的场景(如限制并发请求数),这个开关很实用。

Note

不设上限时,Parallel 会按可用核心自动调度。手动限制常用于”别把数据库或 API 打爆”。

并行里的异常

Parallel.For / PLINQ 中若多个迭代抛异常,会被包成 AggregateException 一次性抛出,而不是第一个异常。

try
{
    Parallel.For(0, 10, i => { if (i == 5) throw new Exception("炸"); });
}
catch (AggregateException ae)
{
    Console.WriteLine($"共 {ae.InnerExceptions.Count} 个异常");
}
Tip

并行代码里别让异常”裸奔”。要么内部处理,要么在外层统一收口,否则多个错误被打包更难定位。

并行与并发:一张对照表

  • 并行(parallel):多核同时执行,针对 CPU 计算。
  • 并发(concurrency):多任务交替推进,常靠异步实现,针对 IO 等待。
  • 目标不同:并行求”算得快”,并发求”等得不卡”。
// 并发:多个 IO 同时等
Task http = FetchAsync("url1");
Task db = QueryAsync("sql");
await Task.WhenAll(http, db); // 两个 IO 重叠进行
Warning

别把”并发 IO”误写成 Parallel.For 去等网络——那会占满线程池线程,反而降低吞吐。

并行不是免费的:Amdahl 直觉

并行加速受”必须串行的部分”限制。若任务里有一半不可避免要串行(比如汇总结果),八核也救不回那一半。

直觉:能并行的比例越高,加核收益越大;串行占比高,加再多核也接近瓶颈。所以先想”能拆多干净”,再谈并行。

Note

这不是要你背公式,而是提醒:并行有天花板,别指望”上并行就一定快几倍”。

任务并行库 TPL 是什么

Task.RunParallelPLINQ 都属”任务并行库(TPL,Task Parallel Library)“。它统一把”任务”交给线程池调度,替你管线程分工。

你基本不必手动 new Thread()。TPL 会根据核心数自动调整并发度,比手管理线程简单且更稳。

// 手动开线程(不推荐)
// new Thread(() => Compute()).Start();

// TPL(推荐)
Task.Run(() => Compute());
Tip

记住:写并行优先用 TPL(Task/Parallel/PLINQ),几乎不要碰裸 Thread。前者有调度、有取消、有异常聚合。

Parallel 的结果与提前退出

Parallel.For 返回 ParallelLoopResult,可看是否被提前中断(比如某次抛错或调了 Stop)。

ParallelLoopResult r = Parallel.For(0, 10, (i, state) =>
{
    if (i == 5) state.Stop(); // 请求停止其余迭代
    Compute(i);
});
Console.WriteLine($"提前结束:{r.IsCompleted == false}");
Warning

并行迭代里若要共享写同一变量,务必用锁或 Interlocked,否则竞态会让结果不可复现。

并行调试小贴士

并行代码”时好时坏”最头疼。几条习惯降低踩坑率:

  1. 循环体先用单线程跑通逻辑,再加并行,便于定位是算法错还是并行错。
  2. 共享状态加锁或用 Interlocked,别靠”应该不会同时”,竞态最不讲道理。
  3. ParallelOptions.MaxDegreeOfParallelism 先限成 1–2,验证逻辑,再放开。
  4. 输出加线程号(Environment.CurrentManagedThreadId)观察是否真并行。
Parallel.For(0, 4, i =>
{
    int tid = Environment.CurrentManagedThreadId;
    Console.WriteLine($"任务 {i} 在线程 {tid}");
});
Warning

并行 bug 难复现,别等上线才暴露。本地用不同并行度多跑几次,比事后救火便宜得多。

一句话区分三种”多”

容易混的三个词,一句话讲清:

  • 多线程:有多个执行流在跑,侧重”机制”。
  • 并行:多线程同时算,侧重”CPU 计算加速”。
  • 并发:多任务交替推进,侧重”IO 等待不空转”。

异步编程主要服务”并发”,并行编程主要服务”CPU 计算”,二者目标不同、工具不同。

Tip

一句话记:等得多用并发(异步),算得多用并行。别拿并行去等网络,也别拿异步去堆 CPU。

为什么需要任务并行库

早期想并行,只能手动 new Thread 开多个线程,自己管生灭、自己收异常、自己分配核。既容易写错,又无法随核心数自适应。

任务并行库(TPL,Task Parallel Library)把”把活拆给多核”标准化:Task.Run 丢给线程池,Parallel 管循环分工,PLINQ 管查询分段。你只描述”做什么”,调度交给运行时。它还会按核心数自动调整并发度,比手管线程省心且更稳。

Note

TPL 不是新东西,而是 Task / Parallel / PLINQ 的总称。凡是”多核同时算”,优先从这里挑工具。

并行与异步:再深入一点

用个比喻加固直觉:

  • 异步像排队打饭时刷手机:队伍没动(IO 在等),你先干别的,不空转。目标是”等得不卡”。
  • 并行像多开几个打饭窗口:一人打要十秒,四个窗口同时打,总耗时砍到约四分之一。目标是”算得快”。

所以:等得多(网络、磁盘)→ 异步并发;算得多(图像处理、数值计算)→ 并行。拿并行去等网络,等于开着窗口却都在发呆等人,纯浪费线程。

Warning

别把”并发 IO”误写成 Parallel.For 去等网络——那会占满线程池线程,反而降低吞吐。IO 用多 Task + await,CPU 用 Parallel。

适用场景:何时选并行

满足以下才值得并行:

  1. 任务是 CPU 密集(大量计算,不是等 IO)。
  2. 各项工作彼此独立,或可安全汇总。
  3. 数据量足够大,能盖过线程调度开销。

不满足就串行——更清晰,也不易出 bug。并行有天花板:核心数有限,拆太碎,调度开销会反超收益。

Tip

先用单线程把逻辑跑通,确认正确后再加并行。并行 bug 难复现,先保证对、再求快。

小结

并行是多核同时算,解决 CPU 密集问题。Task.Run + WhenAll 灵活组合,Parallel.For 专治循环,PLINQ 给查询加速。记住:并行要任务独立,IO 密集请交给异步而非并行。