Unity:将决策卸载到线程以提高效率?

Unity: Offloading decision making to threads for efficiency?

我这里有一个相当大的 Unity 应用程序,它有很多同时发生的事情。我意识到,在 Update() 中完成或调用我的所有评估、迭代、枚举、决策制定和算术逻辑开始对性能产生影响。从那以后,我将大部分逻辑卸载到协程,这又恢复了一些性能,但我正在想我能做些什么来让它变得更好。

协同程序在迭代一些大型集合时恢复了性能,因为我可以在每个循环结束时让步,但在某些情况下,它是一个大型集合,每帧处理一个项目会使这个任务相当慢。这就是我开始将这些类型的任务卸载到线程的地方。在 Unity 中使用线程的问题在于,如果您从线程调用 Unity API,它往往会出现问题。因此,为了解决这个问题,我提出了这种设计模式,其中非 Unity 逻辑在一个线程中完成,然后一旦做出决定或结果,一些额外的 Unity 需要的基于上下文的逻辑在 Unity 的主线程中完成。

public class Evaluator : MonoBehaviour {

   public static Evaluator Instance;

   static Queue<Func<Action>> evaluationQueue = new Queue<Func<Action>>();
   static Queue<Action> actionQueue = new Queue<Action>();
   static Thread evaluatorLoop;

   public static void EvalLogic (Func<Action> evaluation) {
      if (Instance == null) {
         NanoDebug.LogError("EVALUATOR IS NOT INITIALIZED!");
         return;
      }

      evaluationQueue.Enqueue(evaluation);

      if (evaluatorLoop == null) {
         evaluatorLoop = new Thread(EvaluatorLoop);
         evaluatorLoop.IsBackground = true;
         evaluatorLoop.Start();
      }
   }

   static void EvaluatorLoop () {
      for (;;) {
         if (evaluationQueue.Count > 0) {
            Func<Action> evaluation = evaluationQueue.Dequeue();
            Action action = evaluation();
            actionQueue.Enqueue(action);
         }
         else {
            break;
         }
      }
      evaluatorLoop = null;
   }

   void Awake () {
      Instance = this;
   }

   void FixedUpdate () {
      if (actionQueue.Count > 0) {
         actionQueue.Dequeue()?.Invoke();
      }
   }
}

在一些楷书里,我会这样用

void OnSomeGameEvent (GameEventBigData bigData) {
   Func<Action> checkData = () => {
      // Iterate over some big collection.
      // Do X if some condition is met during iteration.
      // Do some arithmetic with data.
      // Evaluate some large if-statement or switch statement.

      // Return this Action based on the decision or result of the above.
      return () => {
         // Mutate the state of some object involving the Unity API using evaluated data.
      };
   };
   Evaluator.EvalLogic(checkData);
}

我已经 运行 遇到一个使用此模式进行大量迭代的类似问题。虽然 Thread 也会立即进行迭代,但如果我需要改变一组游戏对象(每个)的状态,那么上面的动作队列会填满集合的大小,然后达到 FixedUpdate() 应用选定的更改。但是,当涉及到复杂的决策制定或数学计算时,这种模式似乎可以帮助 Unity 不遗余力。

所以我的问题是:我真的通过这种方式获得了性能,还是我最好坚持使用协程并在必要时智能地让步?

这是我对 Update() 的理解。这是一个渲染循环,对吧?如果您想改变游戏对象的状态以改变视觉外观运行ce 或位置,您可以这样做Update()。话虽这么说,然后从 Update()?

卸载 ALL 非渲染相关任务不是更有意义吗?

tl;dr:取决于您的具体用例。

Am I really gaining performace this way, or am I better off sticking to just Coroutines and intelligently yielding when necessary?

这完全取决于线程/协程实际完成的工作以及您使用的线程数量等。

将线程用于性能要求很高的事情(主要是文件 IO、对大型或许多 string 的解析操作或一般的大数据处理)绝对可以提高性能。但是,请记住,创建线程也有一定的过载。如果您要定期创建它们,最好有一个持久线程,然后传入它必须解决的工作包。虽然没有包,但它应该休眠一段时间(例如 17 毫秒,大约是 60 FpS 或更长的帧 - 例如,我通常使用 50 毫秒,现在对结果非常满意)所以它不会在无事可做的情况下占用 100% 的 CPU 时间。

It's a render loop, right?

不是,不是。 Update 只是一条消息,每帧调用一次。你在那里做什么当然取决于你。正如你自己所说,Unity的大部分API(即直接影响场景或资产或依赖它们的这些部分)只能在Unity主线程内使用,所以Unity的任何内置消息(UpdateFixedUpdate、等等)。

That being said, doesn't it make more sense to then offload ALL non-render related tasks from Update()?

没有!那会有点过头了。重的东西,是的,为什么不呢。 但是 请记住,您的设备只有有限数量的并行 CPU 内核来处理所有 ThreadTask。在某一点上,开销只会比实际完成的工作更大。我不会将每个 Vector3 计算移动到一个线程/workPackage 中..但是如果你必须做 1000 个,那么为什么不呢。

如上所述,基本上可以归结为:取决于您的具体用例。


或者 Threads 根据您的需要,您可能还想查看 Unity Job System and the Burst Compiler,它可以执行很多通常以某种方式绑定到 Unity 主线程的异步操作。

特别是它与新的 Mesh API.

结合使用很多

繁重但通用的数学运算的另一种替代方法是使用 Compute Shaders so handing all the work to the GPU. This stuff can get extremly complex but at the same time extremly fast and powerfull. (Again see the examples and times in the MeshAPI examples)


一般来说,这种将 actions/results 分派回主线程是典型的解决方案。只需确保 lock your actionQueue or rater use a ConcurrentQueue 不会陷入线程问题。

此外,您还可以 Coroutine for working the packages off that skips to the next frame if too much time has passed already so you can still achieve a certain target framerate. You could achieve this e.g. using a StopWatch。这当然需要实现某些可能跳过的点(yield),例如在每个工作包之后 / Action.

const float targetFrameRate = 60;

// yes, if you make Start return IEnumerator then Unity automatically
// runs it as a Coroutine
private IEnumerator Start()
{
    // -> about 17 -> actual frame-rate might be only 58 or lower
    var maxMS = Mathf.RoundToInt(1000f / targetFrameRate);

    var stopWatch = new StopWatch();
    stopWatch.Start();
 
    // This is ok in a Coroutine as long as you yield inside
    // in order to at least render one frame eventually
    while(true)
    {
        if (actionQueue.Count > 0) 
        {
            actionQueue.Dequeue()?.Invoke();

            // after each invoked action check if the time has already passed
            if(stopWatch.ElapsedMilliseconds >= targetFrameRate)
            {
                // render this frame and continue int he next
                yield return null;
                // and restart the timer
                stopWatch.Restart();
            }
        }
        else
        {
            // Otherwise directly go to the next frame
            yield return null;
            // and restart the timer
            stopWatch.Restart();
        }
    }
}

如果这仍然不够,并且您的 Action 本身过于密集,您可以将其分成多个 Action,以便再次允许在它们之间跳过。像例如而不是做

Enqueue(LoopThroughObjectsAndDoStuff(thousendObjects));

你更愿意

foreach(var x in thousendObjects) 
{
    Enqueue(()=>DoStuffForObject(x)); 
}

请记住,尽管存在权衡,但您必须在目标帧速率和实时性之间进行一些调整,直到一切都完成。

在某些情况下,您希望用户必须有效地等待更长时间,直到某事完成,但同时让应用程序 运行 具有良好的帧速率。在其他情况下,您允许应用程序 运行 具有较低的帧速率或什至很少冻结,但该过程有效地更快地完成。