C#任务并行库(TPL)
- 注意事项
- 使用举例
-
- 1、并行循环 (Parallel Loops)
- [2、并行 LINQ (PLINQ)](#2、并行 LINQ (PLINQ))
- 3、任务组合
- 4、Task.Run与Task.Factory.StartNew
C# 的任务并行库(Task Parallel Library, TPL)是 System.Threading.Tasks 命名空间下的一组 API,旨在简化并行编程,让开发者能更轻松地利用多核处理器的性能,而无需直接管理底层线程的创建、调度和销毁
| 类别 | 方法/类 | 签名示例 | 适用场景 | 关键特点 |
|---|---|---|---|---|
| 并行循环 | Parallel.For | Parallel.For(0, 100, i => { ... }) | 已知迭代次数的数值循环 | 替代传统 for,自动并行化迭代。注意索引 i 的顺序不保证。 |
| 并行循环 | Parallel.ForEach | Parallel.ForEach(list, item => { ... }) | 遍历集合(List, Array等) | 替代 foreach,对集合元素并行处理。适合元素间无依赖的操作。 |
| 并行 LINQ | AsParallel() | source.AsParallel().Where(...).Sum() | 数据查询与聚合计算 | PLINQ,将 LINQ 查询并行化。适合 CPU 密集型的数据过滤、映射、归约。 |
| 任务启动 | Task.Run | Task.Run(() => { ... }) | 将同步代码卸载到后台线程 | 最常用的异步/并行入口。将工作排队到线程池,返回 Task。 |
| 任务组合 | Task.WhenAll | await Task.WhenAll(task1, task2) | 等待多个任务全部完成 | 并行执行多个独立任务,所有完成后继续。返回结果数组。 |
| 任务组合 | Task.WhenAny | await Task.WhenAny(task1, task2) | 竞速模式/超时控制 | 只要有一个任务完成就返回。常用于冗余请求或超时fallback。 |
| 任务工厂 | TaskFactory | new TaskFactory().StartNew(...) | 高级任务创建与控制 | 比 Task.Run 更底层,可指定 TaskCreationOptions(如 LongRunning)。 |
| 取消支持 | CancellationToken | Parallel.For(..., options, body) | 需要中途停止并行操作 | 配合 ParallelOptions 或 Task 使用,实现优雅的中断机制。 |
| 线程局部存储 | ThreadLocal | new ThreadLocal(() => 0) | 并行循环中的累加/状态保持 | 避免锁竞争。每个线程拥有独立的变量副本,最后合并结果。 |
注意事项
| 关注点 | 详细说明 | 建议做法 |
|---|---|---|
| 任务类型区分 | CPU密集型 vs I/O密集型 是选择并行方案的首要前提。 | CPU密集型:用 Parallel.For/ForEach、PLINQ、Task.Run。 I/O密集型(网络、数据库、文件):用 async/await + Task.WhenAll。 |
| 线程安全 | 并行循环中的多个线程可能同时访问共享资源,导致竞态条件。 | 使用线程安全的并发集合,如 ConcurrentBag<T>、ConcurrentDictionary<K,V>。避免在循环内直接操作普通 List<T>。 |
| 异常处理 | 并行任务中的异常会被包装,不会立即抛出。 | 捕获 AggregateException 并遍历其 InnerExceptions 属性来处理所有异常。 |
| 过度并行化 | 并非任务越多越快,过多的并行会导致上下文切换开销剧增,反而降低性能。 | TPL 会自动根据 CPU 核心数调整并行度。对于 I/O 任务,可使用 SemaphoreSlim 进行限流。 |
| 取消操作 | 长时间运行的并行任务需要支持优雅取消。 | 使用 CancellationToken,并在循环体或任务内部定期检查 token.IsCancellationRequested。 |
| 结果顺序 | Parallel.ForEach 和 PLINQ 默认不保证执行顺序。 |
如果业务依赖顺序,PLINQ 可使用 .AsOrdered(),但会有性能损耗;Parallel.ForEach 则需预分配数组按索引写入。 |
使用举例
1、并行循环 (Parallel Loops)
适用于CPU 密集型且迭代之间无依赖的场景
c#
static async Task Main(string[] args)
{
// 1. Parallel.For: 处理大量数值计算
Parallel.For(10000000, 10000010, i =>
{
// 注意:这里没有顺序保证
CalculatePi(i);
Console.WriteLine("For done threadId={0}, i={1}", Thread.CurrentThread.ManagedThreadId, i);
});
var items = new List<int> { 10000000, 20000000, 40000000, 90000000 };
// 2. Parallel.ForEach: 处理集合
Parallel.ForEach(items, i =>
{
CalculatePi(i);
Console.WriteLine("ForEach done threadId={0}, i={1}", Thread.CurrentThread.ManagedThreadId, i);
});
// 3. 带取消支持和最大并行度的配置
CancellationTokenSource cts = new CancellationTokenSource(100);
CancellationToken token = cts.Token;
var options = new ParallelOptions
{
MaxDegreeOfParallelism = 2, // 限制并发线程数
CancellationToken = token
};
try
{
Parallel.For(400000000, 400000010, options, i =>
{
// 检查取消信号
options.CancellationToken.ThrowIfCancellationRequested();
// if (!token.IsCancellationRequested)
{
CalculatePi(i);
Console.WriteLine("For cancel done threadId={0}, i={1}", Thread.CurrentThread.ManagedThreadId, i);
}
});
}
catch (OperationCanceledException)
{
Console.WriteLine("操作已取消");
}
}
2、并行 LINQ (PLINQ)
适用于数据流处理,语法简洁,但要注意无序性
c#
static async Task Main(string[] args)
{
// 并行查询:筛选偶数并求平方和
// AsParallel() 开启并行
// AsOrdered() 保持顺序(有性能开销)
var result = Enumerable.Range(1, 1000).AsParallel()
.WithDegreeOfParallelism(2) // 可选:限制并行度
.Where(n => n % 2 == 0)
.Select(n => n * n)
.Sum();
Console.WriteLine("done");
}
3、任务组合
适用于异步 I/O 或 混合并行 场景
c#
static async Task Main(string[] args)
{
HttpClient client = new HttpClient();
// 同时发起三个 HTTP 请求
var task1 = client.GetAsync("https://www.baidu.com");
var task2 = client.GetAsync("https://www.bing.com");
var task3 = client.GetAsync("https://www.2345.com");
// 等待所有请求完成
var responses = await Task.WhenAll(task1, task2, task3);
// var response = Task.WhenAny(task1, task2, task3);
// 处理结果
foreach (var response in responses)
{
Console.WriteLine(response.StatusCode);
}
}
4、Task.Run与Task.Factory.StartNew
| 特性 | Task.Run (.NET 4.5+ 引入) | Task.Factory.StartNew (.NET 4.0 引入) |
|---|---|---|
| 默认调度器 | 始终使用线程池 (TaskScheduler.Default),安全地将工作卸载到后台。 |
继承当前上下文调度器。若在 UI 线程调用,可能会意外在 UI 线程执行,导致界面卡死。 |
| 返回值处理 | 自动解包。若传入 async Lambda,返回 Task<T> 而非 Task<Task<T>>。 |
不自动解包。若传入 async Lambda,返回嵌套的 Task<Task>,需额外等待内层任务。 |
| 子任务附加 | 默认使用 DenyChildAttach,禁止子任务附加到当前父任务实例。 |
允许子任务通过 AttachedToParent 选项附加为父任务的子任务。 |
| 适用场景 | 90% 以上的常规后台任务(简单直接)。 | 需要高度自定义的高级场景(如指定自定义调度器、长耗时任务等)。 |
c#
static async Task Main(string[] args)
{
// ✅ 使用 Task.Run:自动解包,直接返回 Task
Task task1 = Task.Run(async () =>
{
await Task.Delay(100); // 异步操作
Console.WriteLine("Task.Run 完成");
});
await task1; // 正确等待内部异步操作完全执行完毕
// ❌ 使用 StartNew:不自动解包,返回 Task<Task>
Task<Task> task2 = Task.Factory.StartNew(async () =>
{
await Task.Delay(100);
Console.WriteLine("StartNew 完成");
});
await task2; // 仅等待外层任务完成!内层异步操作可能尚未执行完!
await task2.Result; // 必须这样写才能正确等待内层任务
}
使用建议
- 默认首选 Task.Run:如果你只是想把一段代码扔到后台执行,或者需要保持 UI 响应性,直接使用 Task.Run(() => {...})。它一行代码就能搞定,且避免了上下文切换和嵌套任务的陷阱
- 仅当你需要精细控制任务配置时才使用它。例如:
- 使用 TaskCreationOptions.LongRunning 标记一个极度耗时的任务,让调度器为其分配专属线程,避免长时间占用线程池资源。
- 需要指定特定的 TaskScheduler(如强制将任务调度回 UI 线程更新控件)
- 需要传递特定的 CancellationToken 或组合复杂的 TaskCreationOptions
c#
// 如果你想用 StartNew 达到与 Task.Run 完全相同的默认安全行为,你需要显式指定四个参数
Task.Factory.StartNew(() =>
{
// TODO
}, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);