Parallel.ForEach 默认采用动态分区策略,线程按需拉取小批量元素(8–64个);显式使用 Partitioner.Create 适用于需连续性、固定块大小、高效范围访问或降低协调开销的场景。

Parallel.ForEach 默认如何分区?
默认情况下,Parallel.ForEach 对 IEnumerable 使用的是「动态分区(dynamic partitioning)」策略:不是一次性把整个集合切分成固定几块,而是由线程在运行时按需从源中“拉取”小批量元素(比如 8–64 个),以减少争用和空闲等待。这种策略对大多数顺序可枚举场景够用,但对索引敏感、需局部缓存或 I/O 密集型操作,容易导致负载不均或重复开销。
什么时候必须显式传入 Partitioner.Create?
以下情况建议绕过默认行为,用 Partitioner.Create 显式控制分区逻辑:
- 源是数组或
IList,且每个分区需保持局部连续性(例如图像分块处理、矩阵行批处理) - 需要固定大小的块(如每次处理 1000 条记录,避免某线程只拿到 3 条)
- 底层数据源本身支持高效范围访问(如数据库游标、内存映射文件),但
IEnumerable包装后丢失了随机访问能力 - 想禁用动态分区带来的内部锁和协调开销(尤其在超低延迟场景)
典型写法是:Partitioner.Create(source, true) —— 第二个参数 true 表示启用静态分区(对数组 / 列表自动按索引切分),比默认动态方式更可预测。
Partitioner.Create 的三个重载怎么选?
关键看数据源类型和是否需要自定义逻辑:
intense图片全屏浏览插件(jQuery),当鼠标点击图片时,可以全屏幕浏览图片,移动鼠标可以查看图片不同的部分,适合相册展示图片细节。兼容主流浏览器,php中文网推荐下载! 使用方法: 1、head区域引用文件styles.css及intense.js 2、在文件中加入区域代码 3、复制images文件夹
-
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 参数在此无效,仍走动态路径;应先转成数组或用第三个重载。
分区器 + Parallel.ForEach 的实际性能陷阱
显式分区不等于性能提升,反而可能引入新问题:
- 分区粒度太粗(如 10 万条一个块):线程数少于 CPU 核心时严重浪费资源;某块耗时远超其他块时整体被拖慢
- 分区粒度太细(如每块 1 条):抵消并行收益,线程调度和锁开销反超计算收益
- 误用
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 行为。








