Task.Run 与并行
本教程共 100 篇 · 第 85 篇 · 更新于 2026-07-31 · 约 7 分钟阅读
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]}");
注意:循环体里不要写同一个共享变量而不加保护,否则结果不确定。
WarningParallel.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}");
适合”数据量大、每项处理独立”的查询。数据量小反而因调度开销更慢。
何时用并行
满足以下条件再考虑并行:
- 任务是 CPU 密集型(大量计算)。
- 各项工作彼此独立,或可安全合并结果。
- 数据量足够大,能盖过线程调度开销。
不满足就别并行——串行更清晰,也不容易出 bug。
Tip别对 IO 操作上 Parallel。读十个网页该用并发异步(多 Task + await),而非 Parallel 占满线程。
并行也有天花板
核心数有限,任务拆太碎,线程切换开销会超过收益。不是核越多越快,也不是任务越碎越好。
普通笔记本四到八核,超过这个并发度的纯计算,边际收益迅速下降。
Note用
Environment.ProcessorCount可查逻辑核心数,作为并行度的参考上限。
控制并行度:ParallelOptions
并行不等于”能开多少开多少”。用 ParallelOptions 的 MaxDegreeOfParallelism 可限制同时跑几个,避免把机器压满。
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.Run、Parallel、PLINQ 都属”任务并行库(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,否则竞态会让结果不可复现。
并行调试小贴士
并行代码”时好时坏”最头疼。几条习惯降低踩坑率:
- 循环体先用单线程跑通逻辑,再加并行,便于定位是算法错还是并行错。
- 共享状态加锁或用
Interlocked,别靠”应该不会同时”,竞态最不讲道理。 - 用
ParallelOptions.MaxDegreeOfParallelism先限成 1–2,验证逻辑,再放开。 - 输出加线程号(
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 管查询分段。你只描述”做什么”,调度交给运行时。它还会按核心数自动调整并发度,比手管线程省心且更稳。
NoteTPL 不是新东西,而是
Task/Parallel/PLINQ的总称。凡是”多核同时算”,优先从这里挑工具。
并行与异步:再深入一点
用个比喻加固直觉:
- 异步像排队打饭时刷手机:队伍没动(IO 在等),你先干别的,不空转。目标是”等得不卡”。
- 并行像多开几个打饭窗口:一人打要十秒,四个窗口同时打,总耗时砍到约四分之一。目标是”算得快”。
所以:等得多(网络、磁盘)→ 异步并发;算得多(图像处理、数值计算)→ 并行。拿并行去等网络,等于开着窗口却都在发呆等人,纯浪费线程。
Warning别把”并发 IO”误写成
Parallel.For去等网络——那会占满线程池线程,反而降低吞吐。IO 用多 Task + await,CPU 用 Parallel。
适用场景:何时选并行
满足以下才值得并行:
- 任务是 CPU 密集(大量计算,不是等 IO)。
- 各项工作彼此独立,或可安全汇总。
- 数据量足够大,能盖过线程调度开销。
不满足就串行——更清晰,也不易出 bug。并行有天花板:核心数有限,拆太碎,调度开销会反超收益。
Tip先用单线程把逻辑跑通,确认正确后再加并行。并行 bug 难复现,先保证对、再求快。
小结
并行是多核同时算,解决 CPU 密集问题。Task.Run + WhenAll 灵活组合,Parallel.For 专治循环,PLINQ 给查询加速。记住:并行要任务独立,IO 密集请交给异步而非并行。