1.15 并行编程

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;

        // 并行遍历数组,每个元素乘以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]}");
    }
}

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),  // 任务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.并行循环原理

并行循环的原理是"分块减少调度开销""线程复用避免创建成本"

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
        {
            // 并行循环:迭代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.停止

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();
            }

            // 关键:检查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.中断

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();
            }

            // 模拟耗时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.取消并行循环

Parallel.For / Parallel.Foreach的取消是通过CancelltionTokenSource控制取消信号的

a.创建CancelltionTokenSource(取消令牌源), 它负责生产CancelltionToken(取消令牌)

b.将CancelltionToken传入ParallelOptions(Parallel的配置参数)

c.当调用CancellationTokenSource.Cancel(), Parallel会在"合适的时机"(比如每次迭代开始前)检查令牌状态, 若已

取消则抛出OperationCanceledException, 终止并行操作

// 1. 创建取消令牌源(可设置超时自动取消或手动触发)
CancellationTokenSource cts = new CancellationTokenSource();

// 方式A:超时自动取消(比如5秒后自动取消并行操作)
cts.CancelAfter(5000); // 5000ms=5秒

// 方式B:手动触发取消(比如Unity中点击“取消”按钮时调用)
void OnCancelButtonClick() {
    if (!cts.IsCancellationRequested) {
        cts.Cancel(); // 触发取消信号
    }
}
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();
}

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), 并行的调度开销会远大于计算收益"

// 测试:累加 1~1000(简单任务 + 小数据量)
var sw = new Stopwatch();

// 1. 串行执行
sw.Restart();
int serialTotal = 0;
for (int i = 1; i <= 1000; i++) serialTotal += i;
sw.Stop();
Console.WriteLine($"串行耗时:{sw.ElapsedTicks} 滴答,结果:{serialTotal}");

// 2. Parallel 并行执行
sw.Restart();
int parallelTotal = 0;
Parallel.For(1, 1001, i => Interlocked.Add(ref parallelTotal, i));
sw.Stop();
Console.WriteLine($"并行耗时:{sw.ElapsedTicks} 滴答,结果:{parallelTotal}");

// 输出(典型结果):
// 串行耗时:~10 滴答
// 并行耗时:~500 滴答(调度开销主导)
2).简单任务 + 大数据量 → 用并行(收益 > 开销)

如果单次任务仍简单, 但数据量极大(1~1 亿), 多核心并行计算的收益会完全覆盖调度开销, 此时并行提速明显

// 测试:累加 1~1 亿(简单任务 + 大数据量)
var sw = new Stopwatch();

// 1. 串行执行
sw.Restart();
long serialTotal = 0;
for (long i = 1; i <= 100_000_000; i++) serialTotal += i;
sw.Stop();
Console.WriteLine($"串行耗时:{sw.ElapsedMilliseconds} ms,结果:{serialTotal}");

// 2. Parallel 并行(带 TLS 优化)
sw.Restart();
long parallelTotal = 0;
Parallel.For<long>(
    1, 100_000_001,
    () => 0, // TLS 初始化
    (i, state, localSum) => localSum + i, // 本地累加(无竞争)
    localSum => Interlocked.Add(ref parallelTotal, localSum) // 原子合并
);
sw.Stop();
Console.WriteLine($"并行耗时:{sw.ElapsedMilliseconds} ms,结果:{parallelTotal}");

// 输出(8 核心 CPU 典型结果):
// 串行耗时:~40 ms
// 并行耗时:~6 ms(提速 6~7 倍)

用Stopwatch对比串行和并行的耗时, 这是最可靠的方式

a.粒度太细(单次任务耗时 < 1 微秒, 如单纯的算术运算): 并行开销占比高, 除非数据量极大

b.粒度适中(单次任务耗时10 ~ 100 微秒以上, 如简单 IO / 解析): 并行收益显著

c.粒度太粗(单次任务耗时 > 100 毫秒, 如大文件读写):需注意并行度, 避免线程数过多导致上下文切换

哪些"简单场景"绝对不用Parallel?

a.数据量极小(< 1000 次迭代), 且单次任务耗时 < 1 微秒

b.任务涉及频繁的共享资源竞争(如频繁加锁), 并行反而会导致"线程阻塞"

c.单核心就能快速完成(如毫秒级任务), 并行调度的耗时比计算本身还长
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值