在C#8/Unity2022中PLINQ组合操作为何串行执行?
问题背景
在C# 8 + Unity 2022环境下,使用PLINQ执行SelectMany+Select+FirstOrDefault组合操作时,出现了串行执行的情况,耗时远超预期:
问题代码
var raw = Enumerable.Range(1, 10); var candidates = raw.AsParallel(); var p1 = candidates.AsParallel() .SelectMany( ii => { Debug.Log(">>>>>> " + ii); Thread.Sleep(1000); Debug.Log("<<<<<< " + ii); return new[] { ii }; } ); var p2 = p1.Select( (v, i) => v); var chosen = p2.FirstOrDefault();
执行结果
日志按顺序串行输出,总耗时10秒:
>>>>>> 1 <<<<<< 1 >>>>>> 2 <<<<<< 2 >>>>>> 3 <<<<<< 3 >>>>>> 4 <<<<<< 4 >>>>>> 5 <<<<<< 5 >>>>>> 6 <<<<<< 6 >>>>>> 7 <<<<<< 7 >>>>>> 8 <<<<<< 8 >>>>>> 9 <<<<<< 9 >>>>>> 10 <<<<<< 10
原因分析
带索引的Select强制有序并行
你使用的Select((v, i) => v)是带索引的重载,这个操作会要求PLINQ严格维护原始序列的顺序——每个元素的索引必须和原始序列的位置对应。为了保证索引的正确性,PLINQ必须等待前面的元素处理完成后,才能为后续元素分配索引,这直接限制了并行执行的可能性,即使添加WithExecutionMode(ParallelExecutionMode.ForceParallelism)也无法完全解除这个限制。FirstOrDefault的终止逻辑影响
FirstOrDefault需要返回序列的第一个元素,在有序并行模式下,PLINQ必须确认前面所有元素都处理完成后,才能确定第一个元素的位置,导致只能串行处理前面的元素,无法同时启动多个任务。PLINQ默认的启发式策略
对于小规模数据集(如这里的10个元素),PLINQ默认会评估并行的开销是否大于收益,可能直接选择串行执行,避免线程调度的额外成本。
修复方案
方案1:移除无意义的带索引Select
如果不需要使用元素索引,直接删除这个冗余的Select操作,同时显式指定无序并行:
var raw = Enumerable.Range(1, 10); var candidates = raw.AsParallel() .WithExecutionMode(ParallelExecutionMode.ForceParallelism) .AsUnordered(); // 取消有序要求,最大化并行性 var p1 = candidates .SelectMany( ii => { Debug.Log(">>>>>> " + ii); Thread.Sleep(1000); Debug.Log("<<<<<< " + ii); return new[] { ii }; } ); var chosen = p1.FirstOrDefault();
方案2:保留Select但改用无索引版本
如果必须保留Select操作,替换为不带索引的版本,并配合无序并行配置:
var raw = Enumerable.Range(1, 10); var candidates = raw.AsParallel() .WithExecutionMode(ParallelExecutionMode.ForceParallelism) .AsUnordered(); var p1 = candidates .SelectMany(ii => { Debug.Log(">>>>>> " + ii); Thread.Sleep(1000); Debug.Log("<<<<<< " + ii); return new[] { ii }; }); var p2 = p1.Select(v => v); // 使用不带索引的Select重载 var chosen = p2.FirstOrDefault();
修复后验证
执行上述修复后的代码,会看到多个>>>>>> x同时输出,总耗时约1秒,符合并行执行的预期,日志示例:
>>>>>> 1 >>>>>> 3 >>>>>> 2 >>>>>> 5 <<<<<< 1 <<<<<< 3 <<<<<< 2 ...
更新情况说明
之前仅添加WithExecutionMode(ParallelExecutionMode.ForceParallelism)未解决问题,核心原因是带索引的Select仍强制有序并行,PLINQ无法完全并行处理所有元素,必须结合AsUnordered()和移除带索引操作才能彻底解决。
内容的提问来源于stack exchange,提问作者tribbloid

