如何不冻结在Unity主线程?

我有一个计算量很大的级别生成algorithm。 因此,调用它总是导致游戏屏幕冻结。 当游戏仍然继续呈现加载屏幕以指示游戏未被冻结时,我怎样才能在第二个线程上放置该function?

更新:在2018年, Unity推出了一个C#作业系统作为卸载工作和使用多个CPU核心的一种方式。

下面的答案早于这个系统。 它仍然可以工作,但现代Unity可能有更好的select,取决于你的需求。 尤其是,作业系统似乎解决了手动创建的线程可以安全访问的一些限制,如下所述。 例如,开发人员尝试使用预览报告执行光线投射和并行构建网格 。

我会邀请有经验的用户使用这个工作系统添加他们自己的答案,反映当前的引擎状态。


我以前在Unity中使用线程来处理重量级的任务(通常是图像和几何处理),并且与在其他C#应用程序中使用线程并没有太大的区别,但有两点需要注意:

  1. 由于Unity使用.NET的一个较老的子集,因此有一些新的线程特性和库不能用于开箱即用,但基本知识都在那里。

  2. 正如Almo在上面的注释中所指出的那样,许多Unitytypes不是线程安全的,并且如果您尝试构造,使用甚至将它们与主线程进行比较,将会抛出exception。 需要注意的是:

    • 一种常见的情况是在尝试访问其成员之前检查GameObject或Monobehaviour引用是否为null。 myUnityObject == null为UnityEngine.Object后代的任何东西调用一个重载的运算符,但是System.Object.ReferenceEquals()可以在一定程度上解决这个问题 – 只要记住一个Destroy()和GameObject与使用重载的null比较,还没有ReferenceEqual为null。

    • 从Unitytypes读取参数在另一个线程上通常是安全的(只要你仔细检查上面的空值,它不会立即抛出exception),但是请注意Philipp的警告 :主线程可能正在修改状态而你正在阅读。 您需要遵守谁被允许修改什么和什么时候,以避免读取一些不一致的状态,这可能会导致错误,可以很难追查,因为它们依赖于线程之间的亚毫秒时间,我们可以不会随意复制。

    • 随机和时间静态成员不可用。 如果需要随机性,则创建每个线程的System.Random实例;如果需要计时信息,则创建System.Diagnostics.Stopwatch。

    • Mathf函数,vector,matrix,四元数和颜色结构在线程中都可以正常工作,所以您可以单独执行大部分计算

    • 创建GameObjects,附加Monobehaviours,或创建/更新纹理,网格,材质等都需要在主线程上进行。 在过去,当我需要使用这些工具时,我建立了一个生产者 – 消费者队列,在这个队列中,我的工作线程准备了原始数据(就像应用于网格或纹理的大量vector/颜色)并在主线程上更新或协程轮询数据并应用它。

有了这些笔记,这里是我经常用于线程工作的模式。 我不能保证这是一个最佳实践的风格,但它完成了工作。 (欢迎提出意见或编辑 – 我知道线程是一个非常深刻的话题,我只知道这些基础知识)

 using UnityEngine; using System.Threading; public class MyThreadedBehaviour : MonoBehaviour { bool _threadRunning; Thread _thread; void Start() { // Begin our heavy work on a new thread. _thread = new Thread(ThreadedWork); _thread.Start(); } void ThreadedWork() { _threadRunning = true; bool workDone = false; // This pattern lets us interrupt the work at a safe point if neeeded. while(_threadRunning && !workDone) { // Do Work... } _threadRunning = false; } void OnDisable() { // If the thread is still running, we should shut it down, // otherwise it can prevent the game from exiting correctly. if(_threadRunning) { // This forces the while loop in the ThreadedWork function to abort. _threadRunning = false; // This waits until the thread exits, // ensuring any cleanup we do after this is safe. _thread.Join(); } // Thread is guaranteed no longer running. Do other cleanup tasks. } } 

如果你不需要为了速度而跨线程分工,而你只是想find一种方法来让它不被阻塞,那么你的游戏的其他部分就会一直保持滴答作响,Unity中更轻量级的解决scheme就是Coroutines 。 这些function可以做一些工作,然后将控制权交还给引擎以继续执行,并在以后进行无缝恢复。

 using UnityEngine; using System.Collections; public class MyYieldingBehaviour : MonoBehaviour { void Start() { // Begin our heavy work in a coroutine. StartCoroutine(YieldingWork()); } IEnumerator YieldingWork() { bool workDone = false; while(!workDone) { // Let the engine run for a frame. yield return null; // Do Work... } } } 

这不需要任何特殊的清理考虑因素,因为引擎(据我所知)从你销毁的对象中删除了协程。

方法的所有本地状态在产生和恢复时都被保留,所以出于多种目的,就好像它在另一个线程上不间断地运行(但是在主线程上有所有的便利)。 你只需要确保它的每个迭代都足够短,不会不合理地减慢你的主线程。

通过确保重要的操作不会被一个良率分开,您可以获得单线程行为的一致性 – 知道主线程上没有其他脚本或系统可以修改正在处理的数据。

收益率回报线给你几个select。 您可以…

  • yield return null ,在下一帧的Update()之后恢复
  • yield return new WaitForFixedUpdate()在下一个FixedUpdate()之后yield return new WaitForFixedUpdate() ()
  • yield return new WaitForSeconds(delay)在经过一定的游戏时间后yield return new WaitForSeconds(delay)
  • 在GUI完成渲染后, yield return new WaitForEndOfFrame()以恢复
  • yield return myRequest ,其中myRequest是一个WWW实例,一旦请求的数据完成从networking或光盘加载,即可恢复。
  • yield return otherCoroutine其中, otherCoroutine是一个协程实例 ,在其他otherCoroutine完成后恢复。 这通常以yield return StartCoroutine(otherCoroutineMethod())的forms用于将执行链接到一个新的协程,该协程本身可以在需要时产生。

取决于你希望协程的下一个回合。

或者yield break; 在协程到达结束之前停止协程,可以使用return; 以提早传统的方法。

你可以将繁重的计算放入另一个线程,但Unity的API不是线程安全的,你必须在主线程中执行它们。

那么你可以尝试在资源商店这个软件包将帮助您更容易地使用线程。 http://u3d.as/wQg您只需使用一行代码即可启动线程并安全执行Unity API。

使用Svelto.Tasks,你可以很容易地把multithreading例程的结果返回给主线程(因此也可以返回统一的function)

http://www.sebaslab.com/svelto-taskrunner-run-serial-and-parallel-asynchronous-tasks-in-unity3d/

@ DMGregory解释得很好。

线程和协程都可以使用。 以前卸载主线程,然后返回控制主线程。 推重吊装螺纹似乎更合理。 因此,工作队列可能是你以后的事情。

Unity Wiki上有非常好的JobQueue示例脚本。