当前位置:首页 > C# > 正文

掌握 C# 自定义并行分区器(深入浅出 .NET 并行编程实战指南)

在现代 C# 开发中,并行编程是提升程序性能的重要手段。.NET 提供了 Parallel.ForEach 等高级 API 来简化多线程任务,但默认的分区策略并不总是最优的。这时,C# 自定义并行分区器 就派上用场了!本文将手把手教你如何创建、测试并理解自定义分区器,即使你是初学者也能轻松上手。

掌握 C# 自定义并行分区器(深入浅出 .NET 并行编程实战指南) 自定义并行分区器  Parallel.ForEach 自定义分区 并行编程教程 Partitioner 示例 第1张

什么是并行分区器?

Parallel.ForEach 中,.NET 需要将数据源“分割”成多个块,分配给不同的线程处理。这个“分割”的逻辑就是由分区器(Partitioner)控制的。

默认情况下,.NET 会根据数据类型(如数组、List)自动选择分区策略。但对于某些特殊场景(例如处理大对象、非均匀数据或自定义集合),默认策略可能导致负载不均,影响性能。此时,我们可以通过 System.Collections.Concurrent.Partitioner 类来自定义分区行为。

为什么需要自定义分区器?

  • 避免线程间负载不均衡(例如某些任务耗时远大于其他)
  • 处理无法随机访问的数据源(如链表、流)
  • 减少锁竞争或内存分配开销
  • 实现特定业务逻辑的分块策略

动手:创建一个简单的自定义分区器

假设我们要处理一个整数列表,但希望每个线程每次只处理固定数量的元素(比如 3 个)。我们可以继承 Partitioner<T> 并重写关键方法。

using System;using System.Collections.Concurrent;using System.Collections.Generic;using System.Threading.Tasks;public class ChunkPartitioner<T> : Partitioner<T>{    private readonly IList<T> _source;    private readonly int _chunkSize;    public ChunkPartitioner(IList<T> source, int chunkSize)    {        _source = source ?? throw new ArgumentNullException(nameof(source));        _chunkSize = chunkSize > 0 ? chunkSize : throw new ArgumentOutOfRangeException(nameof(chunkSize));    }    // 表示是否支持动态分区(通常返回 false)    public override bool SupportsDynamicPartitions => false;    // 返回一个可枚举的分区集合    public override IList<ILookup<int, T>> GetPartitions(int partitionCount)    {        var partitions = new List<ILookup<int, T>>(partitionCount);        for (int i = 0; i < partitionCount; i++)        {            partitions.Add(null!); // 占位        }        return partitions;    }    // 核心方法:返回每个分区的枚举器    public override IEnumerable<IEnumerable<T>> GetDynamicPartitions()    {        throw new NotSupportedException("此分区器不支持动态分区。");    }    // 实现静态分区(推荐用于已知线程数的场景)    public override IList<IEnumerable<T>> GetPartitions(int partitionCount)    {        var partitions = new List<IEnumerable<T>>(partitionCount);        for (int i = 0; i < partitionCount; i++)        {            partitions.Add(GetChunk(i, partitionCount));        }        return partitions;    }    private IEnumerable<T> GetChunk(int partitionIndex, int partitionCount)    {        int startIndex = partitionIndex * _chunkSize;        int endIndex = Math.Min(startIndex + _chunkSize, _source.Count);        for (int i = startIndex; i < endIndex; i++)        {            yield return _source[i];        }    }}

测试自定义分区器

现在我们来编写一个测试程序,验证分区器是否按预期工作:

class Program{    static void Main()    {        var data = Enumerable.Range(1, 20).ToList(); // [1, 2, ..., 20]        var partitioner = new ChunkPartitioner<int>(data, chunkSize: 4);        Console.WriteLine("开始并行处理...");        Parallel.ForEach(            partitioner,            (chunk) =>            {                var threadId = Task.CurrentId ?? 0;                foreach (var item in chunk)                {                    Console.WriteLine($"线程 {threadId} 处理: {item}");                    Thread.Sleep(100); // 模拟耗时操作                }            });        Console.WriteLine("处理完成!");    }}

运行这段代码,你会看到输出结果中,每个线程连续处理 4 个数字(最后一个线程可能少于 4 个)。这说明我们的 C# 自定义并行分区器 成功控制了数据分块逻辑。

性能对比与注意事项

使用自定义分区器前,建议先用默认方式测试性能。只有在确认存在负载不均或性能瓶颈时,才考虑自定义。此外:

  • 确保线程安全(如果数据源被多个线程读取)
  • 避免在分区逻辑中引入过多开销(如频繁内存分配)
  • 对于 Parallel.ForEach,优先使用静态分区(即重写 GetPartitions(int)

结语

通过本教程,你已经掌握了如何在 C# 中实现和测试 .NET 并行编程教程 中的关键组件——自定义分区器。无论是优化现有系统,还是应对复杂数据处理场景,这项技能都将助你一臂之力。记住,Parallel.ForEach 自定义分区 的核心在于“按需分块”,合理使用能显著提升程序效率。

如果你正在寻找更多 C# Partitioner 示例,可以查阅 Microsoft 官方文档或尝试扩展本文中的 ChunkPartitioner 支持动态分区、范围分区等高级功能。

Happy Coding with C# and Parallelism! 🚀