我正在为 Rhino 编写一个插件,它可以对一些光线追踪数据进行动画处理。 Rhino 处理所有绘图;我只需要给它提供一些可以画的东西即可。
使用
Animate
从 UI 线程调用以下 Task.Run(() => Animate(doc, cts.Token, Embree);
函数。该函数内有一个 while
循环,该循环会重复执行,直到抛出 CancellationToken
或 lengthtraveled > maxTimeLength
(均由 UI 线程设置)。 while
循环计算动画的每一帧;即各个点沿着其路径的步骤。当到达边界时,Embree
计算接近光线与边界的交点,并确定反射光线的方向和长度。
我想利用我面前的多核处理器,并将主
for (int i = 0; i < sphereNum; i++)
循环(如下标记)分配给多个并发线程。
目前我在单线程上实现了大约 45Hz 刷新率,但如果它对于能力较差(但仍然是多核)的机器具有更高的性能,那就太好了。
我对
Parallel.For(int fromInclusive, int toExclusive, Action<int> body)
的印象是,它引入了相当多的开销,并且每次调用时都会启动一组新的线程 - 这似乎很浪费,因为我知道相同的过程只会令人厌烦地重复。那么为什么不加载多个线程,每个线程计算一部分点(25,000+),然后等待数据绘制,然后给出命令来计算下一个动画步骤。
这是
Animate
功能:
private void Animate(RhinoDoc doc, CancellationToken cts, EMBContainer Embree)
{
double refreshInterval = 1.0 / 45.0;
int refreshMS = (int)(refreshInterval * 1000.0);
double slowdownFactor = 1.0 / 20.0;
double Speed = 10.0;
double spherestep = refreshInterval * slowdownFactor * Speed;
double lengthtraveled = 0.0;
int sphereNum = 25000;
spheres = new RaySphereData[sphereNum];
points = new Rhino.Geometry.PointCloud(Enumerable.Repeat(StartPoint, sphereNum));
int refreshcounter = 1, resettozero = 0;
long waited = 0;
System.Numerics.Vector3 start, direction;
var vectors = StartingVectors();
start = StartPoint.;
for (int i = 0; i < sphereNum; i++)
{
direction = vectors[i];
Embree.RTCRayHit hits = Embree.Intersect(start, direction);
points[i].Location = StartPoint;
spheres[i].step = direction;
spheres[i].step *= spherestep;
spheres[i].pathlength = hits.Ray.tfar;
spheres[i].done = false;
hits.Hit.Normalize();
spheres[i].nextdirection = Vector3.Reflect(direction, hits.Hit.normal);
spheres[i].nextstart = start + direction * hits.Ray.tfar;
}
Stopwatch stopwatch2 = new Stopwatch();
Stopwatch stopwatch3 = new Stopwatch();
double interimlength;
Stopwatch stopwatch = Stopwatch.StartNew();
Stopwatch stopwatch1 = Stopwatch.StartNew();
while (true)
{
stopwatch2.Start();
if (cts.IsCancellationRequested) return;
var delay = Task.Delay(refreshMS);
doc.Views.Redraw();
lengthtraveled += spherestep;
if (lengthtraveled > maxTimeLength)
{
return;
}
else
{
// start multi-threading here; i.e. send Monitor, Barrier or Semaphore signal to the animation frame compute function
for (int i = 0; i < sphereNum; i++)
{
if (lengthtraveled > spheres[i].pathlength)
{
do
{
interimlength = lengthtraveled - spheres[i].pathlength;
direction = spheres[i].nextdirection;
start = spheres[i].nextstart;
var hits = Embree.Intersect(start, direction);
spheres[i].pathlength += hits.Ray.tfar;
spheres[i].step = direction.ToVector3d();
spheres[i].step.Unitize();
points[i].Location = spheres[i].nextstart.ToPoint3d() + (spheres[i].step * interimlength);
spheres[i].nextstart = start + direction * hits.Ray.tfar;
hits.Hit.Normalize();
spheres[i].nextdirection = Vector3.Reflect(direction, hits.Hit.normal);
}
while (lengthtraveled > spheres[i].pathlength);
spheres[i].step *= spherestep;
}
else
{
points[i].Location += spheres[i].step;
}
}
// end multi-threading
}
stopwatch3.Start();
await delay;
stopwatch3.Stop();
stopwatch2.Stop();
waited += stopwatch3.ElapsedMilliseconds;
stopwatch3.Reset();
stopwatch2.Reset();
if(refreshcounter % 100 == 0)
{
stopwatch1.Stop();
refreshInterval = (stopwatch1.ElapsedMilliseconds - waited) / (1000.0 * refreshcounter);
refreshMS = (int)(refreshInterval * 1000.0);
spherestep = refreshInterval * slowdownFactor * Speed;
for (int i = 0; i < sphereNum; i++)
{
spheres[i].step.Unitize();
spheres[i].step *= spherestep;
}
stopwatch1.Start();
}
refreshcounter++;
}
}
private struct RaySphereData
{
public double pathlength;
public Rhino.Geometry.Vector3d step;
public System.Numerics.Vector3 nextstart;
public System.Numerics.Vector3 nextdirection;
}
我已经研究过
Barrier
、Monitor
和 SemaphoreSlim
,但由于我缺乏理解或这些 .NET 功能的文档太薄弱,我无法使其工作。有什么建议吗? (另外如果对于刷新率的适配还有什么更好的建议,我也非常支持。)
我对
的印象是,它引入了相当多的开销,并且每次调用时都会启动一组新的线程。Parallel.For
你的印象是错误的。默认情况下,
Parallel.For
方法使用当前线程,加上来自ThreadPool
的线程。您可以通过提供自定义 TaskScheduler
来更改默认值,但没有理由这样做。我建议您做的是指定 MaxDegreeOfParallelism
,这样并行循环就不会饱和 ThreadPool
:
ParallelOptions parallelOptions = new()
{
MaxDegreeOfParallelism = Environment.ProcessorCount
};
Parallel.For(0, 25000, parallelOptions, i =>
{
// ...
});
如果您的应用程序大量使用
ThreadPool
,您可以考虑使用 ThreadPool.SetMinThreads
API 增加立即创建线程的阈值。
那么为什么不加载多个线程,每个线程计算一部分点[...]?
因为您的自定义方法不太可能比 .NET 标准库提供的工具更好。就性能、行为或正确性或以上所有方面而言,很可能会更糟。