C# Parallel.For的使用
1 缘起
实验室接了一个新的项目,需要在一块3d地形上基于速度绘上颜色,而且要动态改变。由于地形比较大,mesh网格的顶点一共有一千三百多万个,即使在逻辑上优化之后也达到了一百多万个。使用传统的for循环为每个顶点赋颜色值,效率太低,最终的fps只能达到8左右,完全达不到项目要求。在老师的指导下,开始考虑多线程的方法提高效率。
2 Parallel.For
在查阅了大量网络资源之后,确定了最简单易行的方法是C#本身提供的Parallel并行计算方法。
关于Parallel.For的语法就不细说了,直接通过下面的代码见识一下并行的威力吧。
using System.Diagnostics;
using System.Threading.Tasks;
using UnityEngine;
public class testParallel : Mono Behaviour
{
Stopwatch stopWatch = new Stopwatch();
void Start()
{
stopWatch.Start();
for (int i = 0; i < 10000; i++)
{
for (int j = 0; j < 60000; j++)
{
int sum = 0;
sum += i;
}
}
stopWatch.Stop();
print("NormalFor run " + stopWatch.ElapsedMilliseconds + " ms.");
stopWatch.Reset();
stopWatch.Start();
Parallel.For(0, 10000, item =>
{
for (int j = 0; j < 60000; j++)
{
int sum = 0;
sum += item;
}
});
stopWatch.Stop();
print("ParallelFor run " + stopWatch.ElapsedMilliseconds + " ms.");
}
}
在unity中运行该代码,其结果如下图所示
可以发现,Parallel.For相较于传统的for循环运行时间加快了1s。而在本次项目中,在使用了Parallel.For的情况下,fps直接达到了23,这是一个相当大的提升了。
3 深入思考
在体验到Parallel.For的巨大优势之后,我不禁思考:
- 是不是所有的Parallel.For都比传统的for循环都快呢?
- 今后我是不是可以把所有的for循环都替换为Parallel.For呢?
带着这样的问题,我继续搜索,也发现了使用Parallel.For的一些注意事项。
3.1 Parallel.For的快是有前提的
众所周知,在实现多线程时,为了防止多个线程同时处理同一个变量而导致变量处于"薛定谔状态",引入了"锁"的概念,即在每一时刻只有获得"锁"的线程才能操作目标变量。那么如果在Parallel.For中也需要操作一个全局变量,就意味着即使这是并行计算,大家也需要排队操作全局变量,此时Parallel.For可能远远不如传统的for循环来的快。
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Threading.Tasks;
using UnityEngine;
public class testParallel : MonoBehaviour
{
Stopwatch stopWatch = new Stopwatch();
void Start()
{
var obj = new Object();
long num = 0;
ConcurrentBag<long> bag = new ConcurrentBag<long>();
stopWatch.Start();
for (int i = 0; i < 10000; i++)
{
for (int j = 0; j < 60000; j++)
{
num++;
}
}
stopWatch.Stop();
print("NormalFor run " + stopWatch.ElapsedMilliseconds + " ms.");
stopWatch.Reset();
stopWatch.Start();
Parallel.For(0, 10000, item =>
{
for (int j = 0; j < 60000; j++)
{
lock (obj)
{
num++;
}
}
});
stopWatch.Stop();
print("ParallelFor run " + stopWatch.ElapsedMilliseconds + " ms.");
}
}
在unity中运行该代码,其结果如下图所示
可以发现,Parallel.For的运行时间竟然是传统for循环运行时间的100倍!因此可以得出结论:
如果在循环中需要竞争资源,用到线程锁的话,Parallel.For未必优于传统for循环
3.2 Parallel.For的循环是无序的
由于Parallel.For中的各个循环是同时进行的,所以每次循环体执行的时间会有些许差异,这就导致了循环的运行是无序的。
using System.Threading.Tasks;
using UnityEngine;
public class testParallel : MonoBehaviour
{
void Start()
{
Parallel.For(0, 5, item =>
{
print(item);
});
}
}
在unity中运行该代码,其结果如下图所示
因此可以得出结论:
如果循环的执行顺序需要严格控制的话,则不能使用Parallel.For
4 总结
在计算机多核处理器普及的前提下,合理的使用多线程或者并行能够显著提高软件的运行效率。当然,这一切都是有前提的。滥用多线程往往会适得其反,不仅得不到理想的输出结果,甚至会干扰其他程序的正常运行。在明确了自己的需求的前提下,选择合适的方法才是最重要的。