Task 是 TPL 基础执行单元,代表可能异步完成的操作;优先用 Task.Run 而非 new Task().Start();默认不捕获同步上下文,需 ConfigureAwait(true) 回 UI;TaskScheduler 控制调度策略;Parallel 和 PLINQ 提供高层并行封装,但各有适用场景与限制。

Task 类是 TPL 的基础执行单元
所有异步工作最终都封装在 Task 或 Task 实例中。它不等同于线程,而是“一个可能异步完成的操作”的抽象——可以由线程池线程、IOCP、甚至同步执行(比如 Task.FromResult(42))来完成。
常见误区是认为 new Task(...).Start() 是推荐写法,实际上应优先用静态工厂方法:
Task.Run(() => DoWork()); // 推荐:自动调度到线程池 Task.Factory.StartNew(() => DoWork()); // 较老 API,需注意默认调度器和参数重载歧义 Task.CompletedTask; // 零开销已完成任务,替代 Task.FromResult(())
注意:Task 默认不捕获同步上下文(如 UI 线程),若需回到原始上下文(如 WinForms/WPF 更新控件),得显式用 await task.ConfigureAwait(true)。
TaskScheduler 控制任务如何排队与执行
TaskScheduler 是任务调度策略的抽象。默认是 ThreadPoolTaskScheduler,但你可以自定义,比如实现单线程调度器用于串行化 UI 相关操作,或限制并发数的调度器。
关键点:
-
TaskScheduler.Current返回当前上下文的调度器(常用于Task.Factory.StartNew的taskScheduler:参数) -
TaskScheduler.Default指向线程池调度器 - 自定义调度器必须重写
QueueTask和GetScheduledTasks(后者仅调试用途,可抛NotSupportedException)
不要直接 new 一个 TaskScheduler 并调用 QueueTask——它不保证执行,只是入队;真正触发执行依赖调度器内部循环或外部驱动(如 Windows 消息泵)。
Parallel 类提供数据并行的高层封装
Parallel.For、Parallel.ForEach 和 Parallel.Invoke 是面向集合/范围的并行构造,底层仍基于 Task,但自动处理分区、负载均衡、异常聚合(抛 AggregateException)。
使用时注意:
- 迭代体必须是无副作用或线程安全的;不要在循环里直接修改共享变量,改用
ParallelOptions.MaxDegreeOfParallelism限流或ConcurrentBag等线程安全集合 -
Parallel.ForEach(source, item => { ... })中的source若是IEnumerable,每次枚举都重新遍历——避免传入含副作用的迭代器(如数据库游标) - 它不返回结果集;需要结果请改用 PLINQ(
AsParallel().Select(...).ToArray())
PLINQ 是 Parallel + LINQ,不是独立组件而是 TPL 的查询扩展
AsParallel() 将 IEnumerable 转为 ParallelQuery,后续 LINQ 操作(Where、Select、Sum 等)自动并行化。但它不是万能加速器:
- 小数据集(x => x * 2)反而因分区开销变慢
- 顺序敏感操作(如
First()、Take(1))会退化为部分并行,且结果不保证稳定 - 必须显式调用
AsOrdered()才保持源顺序,但代价是降低吞吐量
错误示例:list.AsParallel().Select(x => riskySideEffect()).ToList() —— 副作用无法预测执行时机和次数,极易引发竞态。
真正难的不是记住这些组件名,而是判断该用 Task.Run 还是 Parallel.ForEach,或者干脆避开 TPL 改用 ValueTask 或 IAsyncEnumerable。线程调度、上下文捕获、异常传播、取消令牌传递——每个环节漏掉一个细节,都会让并行逻辑在高负载下间歇性失败。










