Parallel.ForEach 默认采用动态分区策略,线程按需拉取小批量元素(8–64个);显式使用 Partitioner.Create 适用于需连续性、固定块大小、高效范围访问或降低协调开销的场景。
默认情况下,Parallel.ForEach 对 IEnumerable 使用的是「动态分区(dynamic partitioning)」策略:不是一次性把整个集合切分成固定几块,而是由线程在运行时按需从源中“拉取”小批量元素(比如 8–64 个),以减少争用和空闲等待。这种策略对大多数顺序可枚举场景够用,但对索引敏感、需局部缓存或 I/O 密集型操作,容易导致负载不均或重复开销。
以下情况建议绕过默认行为,用 Partitioner.Create 显式控制分区逻辑:
IList,且每个分区需保持局部连续性(例如图像分块处理、矩阵行批处理)IEnumerable 包装后丢失了随机访问能力典型写法是:Partitioner.Create(source, true) —— 第二个参数 true 表示启用静态分区(对数组 / 列表自动按索引切分),比默认动态方式更可预测。
关键看数据源类型和是否需要自定义逻辑:
Partitioner.Create(TSource[] source, bool loadBalance):最常用。数组 + loadBalance: false → 每个线程分到连续大块;true → 类似默认动态,但基于索引调度Partitioner.Create(IEnumerable source) :仅当源本身已实现高效枚举(如自定义 IEnumerator 支持 Reset 或分段)才考虑,否则可能引发重复枚举或线程不安全Partitioner.Create(int fromInclusive, int toExclusive, int rangeSize):纯索引区间分区,适合配合外部数据结构(如 Span 或数组下标计算),不依赖具体集合实例错误用法示例:对非数组的 List 直接传 Partitioner.Create(list, true) —— 虽然能编译,但 true 参数在此无效,仍走动态路径;应先转成数组或用第三个重载。
显式分区不等于性能提升,反而可能引入新问题:
Partitioner.Create(source, false) 处理非数组源:触发 NotSupportedException,因为只有数组和某些 IList 实现支持静态索引分区var data = Enumerable.Range(0, 100000).ToArray();
// ✅ 推荐:固定块大小,每块 1000 项,静态切分
var partitioner = Partitioner.Create(0, data.Length, 1000);
Parallel.ForEach(partitioner, range => {
for (int i = range.Item1; i < range.Item2; i++) {
Process(data[i]);
}
});
真正难的是平衡「局部性」「负载均衡」「初始化成本」三者——多数项目卡在这一步,不是不会写,而是没测过不同 rangeSize 下的吞吐和 GC 行为。