1.并行循环基本语法
2.并行循环原理
3.并行循环中的异常处理
4.停止
5.中断
6.取消并行循环
1.并行循环基本语法
csharp
复制代码
C#中的Parallel类(位于System.Threading.Tasks命名空间)是.NET提供的并行编程核心工具, 旨在简化"数据并行"和
"任务并行"开发, 充分利用多核CPU资源, 避免手动管理线程的复杂度; 它的核心目标是将串行执行的任务(如循环、独立方
法)"自动拆分为多个并行任务, 复用线程池线程执行", 提升CPU密集型任务的效率
csharp
复制代码
1).Parallel.For: 并行版for循环
替代传统的串行for循环, 将循环迭代拆分为多个并行任务执行, 适合"遍历连续整数范围"的场景
csharp
复制代码
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;
// 并行遍历数组,每个元素乘以2(CPU密集型)
Parallel.For(0, data.Length, i =>
{
data[i] *= 2;
// 打印线程ID,验证并行(不同迭代可能在不同线程执行)
if (i % 1000 == 0)
Console.WriteLine($"迭代{i},线程ID: {System.Threading.Thread.CurrentThread.ManagedThreadId}");
});
Console.WriteLine($"第一个元素: {data[0]}, 最后一个元素: {data[9999]}");
}
}
csharp
复制代码
2).Parallel.Foreach: 并行版foreach
替代传统的串行foreach, 遍历实现了IEnumerable的集合(如 List、数组、Dictionary), 适合"遍历非连续集合"的场景
csharp
复制代码
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})");
});
}
}
csharp
复制代码
3).Parallel.Invoke: 并行执行多个独立的任务
用于一次性执行多个无返回值、无参数的独立方法, 适合"多任务并行执行"场景
csharp
复制代码
using System;
using System.Threading.Tasks;
class ParallelDemo
{
static void Main()
{
// 并行执行两个独立方法,无需关注执行顺序
Parallel.Invoke(
() => CalculateSum(1, 1000000), // 任务1:计算1到100万的和
() => PrintMessage("Hello Parallel") // 任务2:打印信息
);
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.并行循环原理
csharp
复制代码
并行循环的原理是"分块减少调度开销"和"线程复用避免创建成本"
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.并行循环中的异常处理
csharp
复制代码
并行循环中的异常不会立即停止本次迭代, 而是停止新的迭代; 将try catch放在并行循环的外面
csharp
复制代码
using System;
using System.Threading.Tasks;
class ParallelExceptionBasic
{
static void Main()
{
try
{
// 并行循环:迭代1和3抛出不同异常
Parallel.For(0, 5, i =>
{
Console.WriteLine($"迭代{i}开始执行");
if (i == 1)
throw new ArgumentException($"参数非法:迭代{i}"); // 业务异常1
if (i == 3)
throw new DivideByZeroException($"除零错误:迭代{i}"); // 业务异常2
Thread.Sleep(100); // 模拟业务逻辑
});
}
// 必须捕获AggregateException,而非单个异常
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.停止
csharp
复制代码
Stop是"紧急停止" ------ 不管索引顺序, 新迭代全不调度, 已开始的迭代也建议尽快退出(而非执行完)
csharp
复制代码
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();
}
// 关键:检查IsStopped,尽快退出(Stop的核心)
if (state.IsStopped)
{
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 迭代{i} 检测到Stop,立即退出");
return; // 不执行后续的500ms延迟
}
// 若没检测IsStopped,才会执行完500ms
Task.Delay(500).Wait();
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 迭代{i} 执行完成");
});
Console.WriteLine($"循环是否完成:{result.IsCompleted}");
Console.WriteLine($"最低中断迭代索引:{result.LowestBreakIteration ?? -1}\n");
}
}
5.中断
csharp
复制代码
Break是"有序中断" ------ 保证中断点前的迭代全执行完, 只停后面的
csharp
复制代码
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();
}
// 模拟耗时500ms的业务逻辑(Break下会执行完)
Task.Delay(500).Wait();
Console.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 迭代{i} 执行完成");
});
Console.WriteLine($"循环是否完成:{result.IsCompleted}");
Console.WriteLine($"最低中断迭代索引:{result.LowestBreakIteration}\n");
}
}
6.取消并行循环
csharp
复制代码
Parallel.For / Parallel.Foreach的取消是通过CancelltionTokenSource控制取消信号的
a.创建CancelltionTokenSource(取消令牌源), 它负责生产CancelltionToken(取消令牌)
b.将CancelltionToken传入ParallelOptions(Parallel的配置参数)
c.当调用CancellationTokenSource.Cancel()时, Parallel会在"合适的时机"(比如每次迭代开始前)检查令牌状态, 若已
取消则抛出OperationCanceledException, 终止并行操作
csharp
复制代码
// 1. 创建取消令牌源(可设置超时自动取消或手动触发)
CancellationTokenSource cts = new CancellationTokenSource();
// 方式A:超时自动取消(比如5秒后自动取消并行操作)
cts.CancelAfter(5000); // 5000ms=5秒
// 方式B:手动触发取消(比如Unity中点击"取消"按钮时调用)
void OnCancelButtonClick() {
if (!cts.IsCancellationRequested) {
cts.Cancel(); // 触发取消信号
}
}
csharp
复制代码
try {
// 2. 配置Parallel的参数,传入取消令牌
ParallelOptions options = new ParallelOptions();
options.CancellationToken = cts.Token; // 绑定取消令牌
// 3. 执行并行操作(以Parallel.For为例)
Parallel.For(0, 1000, options, (i, loopState) =>
{
if(options.CancellationToken.IsCancellationRequested)
{
return;
}
// 你的并行任务逻辑(比如处理数据、加载资源)
Debug.Log($"处理第{i}项");
Thread.Sleep(100); // 模拟耗时操作
});
}
// 步骤3:捕获取消异常,处理取消逻辑
catch (OperationCanceledException ex) {
Debug.Log($"并行操作已取消:{ex.Message}");
}
catch (AggregateException ex) {
// Parallel的异常会包装成AggregateException,需展开处理
foreach (var innerEx in ex.InnerExceptions) {
if (innerEx is OperationCanceledException) {
Debug.Log($"并行操作已取消");
} else {
Debug.Log($"并行操作出错:{innerEx.Message}");
}
}
}
finally {
// 释放资源
cts.Dispose();
}