1.并行循环基本语法
2.并行循环原理
3.并行循环中的异常处理
4.停止
5.中断
6.取消并行循环
7.线程本地存储
8.性能考虑因素
1.并行循环基本语法
C#中的Parallel类(位于System.Threading.Tasks命名空间)是.NET提供的并行编程核心工具, 旨在简化"数据并行"和
"任务并行"开发, 充分利用多核CPU资源, 避免手动管理线程的复杂度; 它的核心目标是将串行执行的任务(如循环、独立方
法)"自动拆分为多个并行任务, 复用线程池线程执行", 提升CPU密集型任务的效率
1).Parallel.For: 并行版for循环
替代传统的串行for循环, 将循环迭代拆分为多个并行任务执行, 适合"遍历连续整数范围"的场景

using System;
using System.Threading.Tasks;
class ParallelForDemo
{
static void Main()
{
int[] data = new int[10000];
for (int i = 0; i < data.Length; i++) data[i] = i;
Parallel.For(0, data.Length, i =>
{
data[i] *= 2;
if (i % 1000 == 0)
Console.WriteLine($"迭代{i},线程ID: {System.Threading.Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"第一个元素: {data[0]}, 最后一个元素: {data[9999]}");
}
}
2).Parallel.Foreach: 并行版foreach
替代传统的串行foreach, 遍历实现了IEnumerable的集合(如 List、数组、Dictionary), 适合"遍历非连续集合"的场景
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class ParallelForEachDemo
{
static void Main()
{
List<string> fruits = new List<string> { "Apple", "Banana", "Orange", "Grape", "Mango" };
Parallel.ForEach(fruits, fruit =>
{
string upperFruit = fruit.ToUpper();
Console.WriteLine($"处理结果: {upperFruit} (线程ID: {System.Threading.Thread.CurrentThread.ManagedThreadId})");
});
}
}
3).Parallel.Invoke: 并行执行多个独立的任务
用于一次性执行多个无返回值、无参数的独立方法, 适合"多任务并行执行"场景
using System;
using System.Threading.Tasks;
class ParallelDemo
{
static void Main()
{
Parallel.Invoke(
() => CalculateSum(1, 1000000),
() => PrintMessage("Hello Parallel")
);
Console.WriteLine("所有并行任务执行完成");
}
static void CalculateSum(int start, int end)
{
long sum = 0;
for (int i = start; i <= end; i++) sum += i;
Console.WriteLine($"Sum: {sum} (线程ID: {System.Threading.Thread.CurrentThread.ManagedThreadId})");
}
static void PrintMessage(string msg)
{
Console.WriteLine($"{msg} (线程ID: {System.Threading.Thread.CurrentThread.ManagedThreadId})");
}
}
2.并行循环原理
并行循环的原理是"分块减少调度开销"和"线程复用避免创建成本"
a.数据分区(分块) —— 不是"均分", 而是"动态按需分块"
并行循环首先会将待处理的数据集(比如0~999的迭代、List集合)拆分为若干"分区(Chunk)", 但不是静态均分, 而是由.NET
的"分区器(Partitioner)"动态调整
- 静态分区(适用于迭代执行时间均匀的场景)
启动前将数据均分(比如1000个迭代, 4核CPU拆成4块, 每块250 个), 优点是分区开销小, 缺点是如果某块迭代执行慢(比如
处理大数据), 会导致"有的线程闲、有的线程忙"(负载不均)
- 动态分区(Parallel默认策略)
不提前均分, 而是"按需分配小批次"(比如每次分配 10~20 个迭代为一个小块), 线程处理完当前小块后, 立刻去"领取"下一
个小块, 直到所有数据处理完
✅优势: 解决负载不均问题(比如某块迭代执行慢, 其他线程不会等, 继续领新块), 最大化CPU利用率
b.线程调度 —— 复用线程池, 而非创建新线程
Parallel循环不会为每个块创建新线程, 而是复用.NET"线程池(ThreadPool)"的工作线程
线程池默认有"最小线程数(= CPU 核心数)"和"最大线程数(默认 1023)", Parallel会向线程池请求线程, 而非手动创建(避
免线程创建 / 销毁的昂贵开销)
并行度(同时运行的线程数)默认由".NET根据CPU核心数、当前系统负载动态调整"
c.执行与线程复用 —— 一个线程处理多个块
线程与块不是一一绑定, 一个线程处理完一个小块后, 不会销毁, 而是立刻从分区器领取下一个小块继续执行; 直到所有小块
处理完毕, 线程才会回到线程池, 等待后续复用
d.收尾 —— 合并结果(如有) + 处理异常
- 若有共享结果(比如累加求和), 需通过原子操作 / 锁
- 若多个块抛出异常, 会封装为AggregateException统一抛出
3.并行循环中的异常处理
并行循环中的异常不会立即停止本次迭代, 而是停止新的迭代; 将try catch放在并行循环的外面
using System;
using System.Threading.Tasks;
class ParallelExceptionBasic
{
static void Main()
{
try
{
Parallel.For(0, 5, i =>
{
Console.WriteLine($"迭代{i}开始执行");
if (i == 1)
throw new ArgumentException($"参数非法:迭代{i}");
if (i == 3)
throw new DivideByZeroException($"除零错误:迭代{i}");
Thread.Sleep(100);
});
}
catch (AggregateException aggregateEx)
{
Console.WriteLine($"捕获到 {aggregateEx.InnerExceptions.Count} 个异常:");
foreach (var innerEx in aggregateEx.InnerExceptions)
{
switch (innerEx)
{
case ArgumentException argEx:
Console.WriteLine($"参数异常:{argEx.Message}");
break;
case DivideByZeroException divEx:
Console.WriteLine($"除零异常:{divEx.Message}");
break;
default:
Console.WriteLine($"未知异常:{innerEx.Message}");
break;
}
}
}
catch (Exception ex)
{
Console.WriteLine($"非聚合异常:{ex.Message}");
}
}
}
4.停止
Stop是"紧急停止" —— 不管索引顺序, 新迭代全不调度, 已开始的迭代也建议尽快退出(而非执行完)
using System;
using System.Threading.Tasks;
class ParallelBreakVsStop
{
static void Main()
{
Console.WriteLine("=== 测试 Stop() ===");
var result = Parallel.For(0, 10, (i, state) =>
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 迭代{i} 开始执行");
if (i == 5)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 触发 Stop()");
state.Stop();
}
if (state.IsStopped)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 迭代{i} 检测到Stop,立即退出");
return;
}
Task.Delay(500).Wait();
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 迭代{i} 执行完成");
});
Console.WriteLine($"循环是否完成:{result.IsCompleted}");
Console.WriteLine($"最低中断迭代索引:{result.LowestBreakIteration ?? -1}\n");
}
}

5.中断
Break是"有序中断" —— 保证中断点前的迭代全执行完, 只停后面的
using System;
using System.Threading.Tasks;
class ParallelBreakVsStop
{
static void Main()
{
Console.WriteLine("=== 测试 Break() ===");
var result = Parallel.For(0, 10, (i, state) =>
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 迭代{i} 开始执行");
if (state.ShouldExitCurrentIteration && state.LowestBreakIteration >= index)
{
Console.WriteLine("检测到并行循环中触发Stop, 应立即停止");
return;
}
if (i == 5)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 触发 Break()");
state.Break();
}
Task.Delay(500).Wait();
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 迭代{i} 执行完成");
});
Console.WriteLine($"循环是否完成:{result.IsCompleted}");
Console.WriteLine($"最低中断迭代索引:{result.LowestBreakIteration}\n");
}
}

6.取消并行循环
Parallel.For / Parallel.Foreach的取消是通过CancelltionTokenSource控制取消信号的
a.创建CancelltionTokenSource(取消令牌源), 它负责生产CancelltionToken(取消令牌)
b.将CancelltionToken传入ParallelOptions(Parallel的配置参数)
c.当调用CancellationTokenSource.Cancel()时, Parallel会在"合适的时机"(比如每次迭代开始前)检查令牌状态, 若已
取消则抛出OperationCanceledException, 终止并行操作
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
void OnCancelButtonClick() {
if (!cts.IsCancellationRequested) {
cts.Cancel();
}
}
try {
ParallelOptions options = new ParallelOptions();
options.CancellationToken = cts.Token;
Parallel.For(0, 1000, options, (i, loopState) =>
{
if(options.CancellationToken.IsCancellationRequested)
{
return;
}
Debug.Log($"处理第{i}项");
Thread.Sleep(100);
});
}
catch (OperationCanceledException ex) {
Debug.Log($"并行操作已取消:{ex.Message}");
}
catch (AggregateException ex) {
foreach (var innerEx in ex.InnerExceptions) {
if (innerEx is OperationCanceledException) {
Debug.Log($"并行操作已取消");
} else {
Debug.Log($"并行操作出错:{innerEx.Message}");
}
}
}
finally {
cts.Dispose();
}
7.线程本地存储
a.如果直接在Parallel循环中操作共享变量, 会因多线程竞争导致数据错误, 必须加锁(如lock), 但锁会严重降低并行性能
b.核心解决思路:
让每个线程先维护"本地累加器(TLS)", 迭代完成后再将本地结构合并到全局变量(仅合并时需要线程安全), 大幅减少锁竞争
c.Parallel.For提供了TLS的支持, 核心参数分为3部分:
Parallel.For<TLocal>(
int fromInclusive,
int toExclusive,
Func<TLocal> localInit,
Func<int, ParallelLoopState, TLocal, TLocal> body,
Action<TLocal> localFinally
);

var arrs = Enumerable.Range(0, 1000).ToArray();
var sum = 0;
var now = DateTime.Now;
Parallel.For(0,
arrs.Length,
() =>
{
return 0;
},
(idx, state, tls) =>
{
tls += arrs[idx];
return tls;
},
(tls) =>
{
lock (obj)
{
sum += tls;
}
}
);
Console.WriteLine($"并行循环完毕 sum: {sum}, costTime: {(DateTime.Now - now).TotalMilliseconds}");
8.性能考虑因素
Parallel的核心价值是利用多CPU核心提升吞吐量, 但并行本身存在不可忽视的开销; 如果任务足够"简单"(单次执行耗时短)
且数量量小, 并行的开销会抵消甚至超过收益, 此时串行反而更快
1).简单任务 + 小数据量 → 不用并行(串行更快)
"如果单次任务是极简单的操作(如 i+1、num*2), 且数据量小(如 1 ~ 1000), 并行的调度开销会远大于计算收益"
var sw = new Stopwatch();
sw.Restart();
int serialTotal = 0;
for (int i = 1; i <= 1000; i++) serialTotal += i;
sw.Stop();
Console.WriteLine($"串行耗时:{sw.ElapsedTicks} 滴答,结果:{serialTotal}");
sw.Restart();
int parallelTotal = 0;
Parallel.For(1, 1001, i => Interlocked.Add(ref parallelTotal, i));
sw.Stop();
Console.WriteLine($"并行耗时:{sw.ElapsedTicks} 滴答,结果:{parallelTotal}");
2).简单任务 + 大数据量 → 用并行(收益 > 开销)
如果单次任务仍简单, 但数据量极大(如 1~1 亿), 多核心并行计算的收益会完全覆盖调度开销, 此时并行提速明显
var sw = new Stopwatch();
sw.Restart();
long serialTotal = 0;
for (long i = 1; i <= 100_000_000; i++) serialTotal += i;
sw.Stop();
Console.WriteLine($"串行耗时:{sw.ElapsedMilliseconds} ms,结果:{serialTotal}");
sw.Restart();
long parallelTotal = 0;
Parallel.For<long>(
1, 100_000_001,
() => 0,
(i, state, localSum) => localSum + i,
localSum => Interlocked.Add(ref parallelTotal, localSum)
);
sw.Stop();
Console.WriteLine($"并行耗时:{sw.ElapsedMilliseconds} ms,结果:{parallelTotal}");
用Stopwatch对比串行和并行的耗时, 这是最可靠的方式
a.粒度太细(单次任务耗时 < 1 微秒, 如单纯的算术运算): 并行开销占比高, 除非数据量极大
b.粒度适中(单次任务耗时10 ~ 100 微秒以上, 如简单 IO / 解析): 并行收益显著
c.粒度太粗(单次任务耗时 > 100 毫秒, 如大文件读写):需注意并行度, 避免线程数过多导致上下文切换
哪些"简单场景"绝对不用Parallel?
a.数据量极小(如 < 1000 次迭代), 且单次任务耗时 < 1 微秒
b.任务涉及频繁的共享资源竞争(如频繁加锁), 并行反而会导致"线程阻塞"
c.单核心就能快速完成(如毫秒级任务), 并行调度的耗时比计算本身还长