如何不冻结Unity中的主线程?


33

我有一个计算量很大的关卡生成算法。因此,调用它总是导致游戏屏幕冻结。在游戏仍继续渲染加载屏幕以指示游戏未冻结时,如何在第二个线程上放置该功能?


1
您可以使用线程系统在后台运行其他作业...但是它们可能不会在Unity API中调用任何内容,因为这些东西不是线程安全的。只是发表评论是因为我还没有这样做,并且不能自信地迅速为您提供示例代码。
Almo

使用List动作将要调用的函数存储在主线程中。锁定并将函数中的Action列表复制Update到临时列表,清除原始列表,然后在主线程上执行Action代码List。有关其他操作,请参见我的其他文章中的 UnityThread 。例如,要在主线程上调用函数, UnityThread.executeInUpdate(() => { transform.Rotate(new Vector3(0f, 90f, 0f)); });
程序员

Answers:


48

更新: 2018年,Unity将推出C#作业系统,以减轻工作负担并利用多个CPU内核。

以下答案早于该系统。它仍然可以工作,但是现代Unity中可能有更好的选择,具体取决于您的需求。特别是,作业系统似乎解决了一些有关手动创建的线程可以安全访问的限制,如下所述。例如,开发人员尝试使用预览报告执行射线广播并并行构造网格

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


过去,我曾在Unity中将线程用于重量级任务(通常是图像和几何处理),与在其他C#应用程序中使用线程没有太大不同,但有两个警告:

  1. 因为Unity使用的是.NET的较旧的子集,所以有些新的线程功能和库是我们无法立即使用的,但基础知识就在那里。

  2. 正如Almo在上面的评论中指出的那样,许多Unity类型不是线程安全的,并且如果您尝试在主线程上构造,使用甚至比较它们,它们将引发异常。注意事项:

    • 一种常见情况是在尝试访问GameObject或Monobehaviour引用之前,先检查其是否为null。myUnityObject == null为从UnityEngine.Object派生的任何东西调用一个重载的运算符,但是System.Object.ReferenceEquals()在某种程度上可以解决此问题-请记住,使用重载将Destroy()ed GameObject进行比较等于null,但尚未将ReferenceEqual等于null。

    • 从Unity类型读取参数通常在另一个线程上是安全的(只要您仔细检查上述空值,它就不会立即引发异常),但是请注意Philipp的警告,即主线程可能正在修改状态在阅读时。为了避免读取不一致的状态,您需要对允许哪些人修改内容和时间进行约束,这可能会导致导致难以确定的错误,因为这些错误取决于我们可以在线程之间进行的毫秒级计时。不能随意复制。

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

    • Mathf函数,向量,矩阵,四元数和颜色结构在线程之间都可以正常工作,因此您可以分别进行大多数计算

    • 创建游戏对象,附加Monobehaviours或创建/更新纹理,网格,材质等都需要在主线程上进行。过去,当我需要使用它们时,我建立了一个生产者-消费者队列,我的工作线程在其中准备原始数据(例如,大量矢量/颜色要应用于网格或纹理),主线程上的Update或Coroutine轮询数据并应用它。

在没有这些注释的情况下,这是我经常用于线程工作的模式。我不保证这是一种最佳实践风格,但是可以完成工作。(欢迎发表评论或进行改进,我知道线程是一个非常深入的主题,我只知道其基础知识)

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.
    }
}

如果您不需要严格地在线程之间分配工作来提高速度,而您只是在寻找一种使其不阻塞的方式,以便让您的游戏其余部分不断发展,那么在Unity中轻巧的解决方案就是Coroutines。这些功能可以完成一些工作,然后将控制权交还给引擎以继续其工作,并在以后的时间无缝恢复。

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...
        }
    }
}

这不需要任何特殊的清理注意事项,因为引擎(据我所知)可以为您消除被破坏对象的协程。

该方法的所有局部状态在产生和恢复时都将保留,因此出于许多目的,就好像它在另一个线程上不间断地运行(但在主线程上运行具有所有便利)。您只需要确保它的每次迭代都足够短,以免不会不合理地减慢主线程的速度。

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

收益率返回行为您提供了一些选择。您可以...

  • yield return null 在下一帧的Update()之后恢复
  • yield return new WaitForFixedUpdate() 在下一个FixedUpdate()之后恢复
  • yield return new WaitForSeconds(delay) 经过一定的游戏时间后恢复播放
  • yield return new WaitForEndOfFrame() 在GUI完成渲染后恢复
  • yield return myRequest这里myRequest是一个WWW例如,为了恢复一旦所请求的数据通过网络或光盘加载完毕。
  • yield return otherCoroutine这里otherCoroutine是一个协程实例,之后继续otherCoroutine完成。这通常用于将yield return StartCoroutine(OtherCoroutineMethod())执行链接到一个新的协程,该协程本身可以在需要时产生。

    • 在实验上,如果要在同一上下文中链接执行,则跳过第二步StartCoroutine并仅yield return OtherCoroutineMethod()完成写入即可达到相同的目标。

      StartCoroutine如果要与第二个对象关联运行嵌套协程,则在a中进行包装可能仍然有用yield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())

...取决于您何时希望协程采取下一轮行动。

或者yield break;在协程结束之前停止协程,这可能是您return;提早使用传统方法的一种方式。


是否只有在迭代花费超过20毫秒后才使用协程产生结果的方法?
DarkDestry

@DarkDestry您可以使用秒表实例来计时您在内部循环中花费的时间,并在重置秒表并恢复内部循环之前,跳出一个外部循环,在该循环中屈服。
DMGregory

1
好答案!我只是想补充一点,使用协程的另一个原因是,如果您曾经需要为webgl构建能够正常工作的东西,而如果您使用线程则不会。现在正坐在一个巨大的项目中,头疼:P
MikaelHögström,2016年

好的答案,谢谢。我只是想知道我是否需要多次停止并重新启动线程,我尝试中止并启动,但出现错误
flankechen

@flankechen线程启动和拆除是相对昂贵的操作,因此,我们通常更喜欢使线程保持可用状态,但保持休眠状态–使用信号量或监视器之类的信号来通知我们何时有新工作要做。是否想发布一个详细描述您的用例的新问题,而人们可以提出有效的实现方法?
DMGregory

0

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

好了,您可以在Asset Store上尝试使用此软件包,这将帮助您更轻松地使用线程。http://u3d.as/wQg您只需使用一行代码即可启动线程并安全地执行Unity API。



0

@DMGregory解释得很好。

线程和协程都可以使用。前者用于卸载主线程,后来用于将控制权返回给主线程。将繁重的工作推到分开的线程似乎更合理。因此,作业队列可能就是您想要的。

Unity Wiki上确实有很好的JobQueue示例脚本。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.