为动画编写重复的并行循环,避免 System.Threading.Tasks.Parallel.For

问题描述 投票:0回答:1

我正在为 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 功能的文档太薄弱,我无法使其工作。有什么建议吗? (另外如果对于刷新率的适配还有什么更好的建议,我也非常支持。)

c# .net task-parallel-library .net-4.8 rhino
1个回答
0
投票

我对

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 标准库提供的工具更好。就性能、行为或正确性或以上所有方面而言,很可能会更糟。

© www.soinside.com 2019 - 2024. All rights reserved.