C#并行编程详解:Tasks.Parallel类核心方法与实战应用
在多核时代,充分利用CPU资源是提升程序性能的关键。C#通过Task Parallel Library (TPL)提供了强大的并行处理能力。本文将重点解析System.Threading.Tasks.Parallel类,演示如何利用Parallel.Invoke并行调用不同方法,使用Parallel.For和Parallel.ForEach加速集合遍历,并通过线程局部变量解决并发累加问题,同时介绍循环中断与异常处理的正确姿势。
并行基础与TPL简介
.NET 4.0引入的并行扩展主要依赖TPL(Task Parallel Library)。与传统多线程需手动创建和管理线程不同,TPL面向“任务”抽象,自动调度线程资源,极大简化了并行开发的复杂度。数据并行是TPL的核心场景之一,指对源集合或数组元素同时执行相同操作。TPL通过System.Threading.Tasks.Parallel类提供实现,开发者只需编写类似串行循环的逻辑,底层会自动进行数据分区与线程分配。
Parallel.Invoke:并行调用多个独立任务
当需要同时执行多个互不依赖的操作时,可使用Parallel.Invoke。该方法接受一组Action委托,它们将在可用线程上并发执行。由于共享线程池,多个任务可能复用相同的线程,并非严格的一对一映射。
static void Main(string[] args)
{
var watch = Stopwatch.StartNew();
// 并行执行Run1和Run2,耗时约为两者中较长者而非总和
Parallel.Invoke(Run1, Run2);
watch.Stop();
Console.WriteLine("并行开发总共耗时:{0} ms", watch.ElapsedMilliseconds);
}
static void Run1()
{
Thread.Sleep(3000);
}
static void Run2()
{
Thread.Sleep(5000);
}
为了验证线程复用情况,可观察以下示例:
ThreadLocal<string> threadName = new ThreadLocal<string>(() =>
{
return "Thread" + Thread.CurrentThread.ManagedThreadId;
});
Action printThread = () =>
{
bool isFirstTime = !threadName.IsValueCreated;
Console.WriteLine($"{threadName.Value} {(isFirstTime ? "(首次)" : "(复用)")} ");
};
// 并行调用8次,通常不会看到8个不同的线程ID
Parallel.Invoke(printThread, printThread, printThread, printThread, printThread, printThread, printThread, printThread);
text-align:center">
p>
Parallel.For:优化For循环性能
对于固定迭代的循环结构,Parallel.For可将迭代任务分发到多个处理器核心并行执行。其返回值ParallelLoopResult包含IsCompleted属性,用于判断循环是否全部完成。
// 范围[0, 10)
ParallelLoopResult result = Parallel.For(0, 10, i =>
{
Console.WriteLine($"迭代{i}, TaskID:{Task.CurrentId}, ThreadID:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(10);
});
Console.WriteLine($"是否全部完成: {result.IsCompleted}");
在嵌套循环中,仅将最外层改为并行可获得显著提速。内部仍保持串行以保证单次迭代的原子性:
private void ParallelOptimization()
{
// 外层并行分发大区间,内层串行处理子逻辑
Parallel.For(0, 10000, i =>
{
for (int j = 0; j < 1000; j++)
{
for (int k = 0; k < 100; k++)
{
DoSomething();
}
}
});
}
Parallel.ForEach:处理集合数据并行
针对IEnumerable集合,Parallel.ForEach更为适用。它能智能地对数据进行分区(Partitioning),每个分区内的计算串行执行,分区之间并行运算,有效减少线程切换开销。
var files = GetFiles();
Parallel.ForEach(files, file =>
{
ProcessFile(file);
});
自定义分区策略同样支持,这在处理非标准集合时非常有用:
ConcurrentBag<int> results = new ConcurrentBag<int>();
// 手动指定数据范围区间 [0, 3000000)
Parallel.ForEach(Partitioner.Create(0, 3000000), range =>
{
for (int m = range.Item1; m < range.Item2; m++)
{
results.Add(m * 2); // 示例计算
}
});
线程局部变量与汇总策略
在并行累加等场景中,直接修改共享变量会导致竞态条件(Race Condition)。正确的做法是使用泛型版本的Parallel.For<TLocal>或Parallel.ForEach<TSource, TLocal>。
参数说明:
TLocal: 线程局部变量的类型。
localInit: 初始化局部变量,每个线程仅调用一次。
body: 循环体逻辑,返回更新后的局部变量值。
localFinally: 线程结束时的回调,用于将各线程结果安全地合并至全局变量(如使用Interlocked类)。
Parallel.For局部变量示例
从数值2开始累加:
int[] nums = Enumerable.Range(0, 10).ToArray();
long totalGlobal = 0;
Parallel.For<long>(0, nums.Length,
() => 2, // localInit: 初始值为2
(j, loopState, subtotal) =>
{
subtotal += nums[j]; // 线程私有累加
Console.WriteLine($"局部累加结果: {subtotal}");
return subtotal; // body返回新状态
},
(finalVal) =>
{
Interlocked.Add(ref totalGlobal, finalVal); // localFinally安全汇总
});
Console.WriteLine($"最终总计: {totalGlobal}");
循环控制:Stop与Break
ParallelLoopState提供了两种终止循环的方式:
- Stop(): 请求立即停止所有正在进行的迭代。已开始的迭代可能会继续运行直至完成当前单元,但不再接收新任务。
- Break(): 声明“我已完成之前所有的迭代”。它会确保小于当前索引的所有迭代都能完整执行,之后停止更大数据的处理。这适用于需要保留前缀结果的场景。
并发取消与异常处理
可通过ParallelOptions.CancellationToken配合CancellationTokenSource来优雅地取消正在进行的并行任务。若任务因取消而退出,通常会抛出OperationCanceledException。
由于并行环境中任何线程都可能抛出异常,单个异常不足以反映全貌。推荐使用ConcurrentQueue<Exception>收集所有遇到的异常,循环结束后抛出一个包装所有异常的AggregateException,以便外部统一捕获和处理。
以上就是C#并行编程之Tasks.Parallel类的详细内容,更多关于C#并行优化与数据处理的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













