一.Unity是否支持多线程?

Unity支持多线程的使用,可以使用C#的Thread类来创建和管理线程,只需要引入这个类:

unity多线程加载场景 unity多线程限制_unity多线程加载场景

但需要注意的是,在Unity中,只有主线程(也称为渲染线程)可以访问Unity对象,如GameObject、Transform等,如果在其他线程中访问这些对象,会导致不可预期的结果。

因此,在使用多线程时,需要遵循一些规则:

  1. 不要在非主线程中访问Unity对象;
  2. 不要在多个线程中同时修改同一个对象或变量,否则可能会导致竞态条件;
  3. 不要过度使用多线程,因为线程切换会带来额外的开销,而且多线程可能会增加代码复杂性和调试难度;
  4. Unity中的多线程记得使用后要关闭,否则会在推出调试后一直运行,直到关闭Unity或者改变相应的脚本代码。

示例: 

unity多线程加载场景 unity多线程限制_主线程_02

 新开的线程虽然不能访问Unity中的对象,但是多线程可以进行一些复杂的逻辑计算:例如A*算法、网格计算,复杂的计算可能会卡住主线程,因此我们用副线程去计算。算好了,主线程再去调用。

示例:

unity多线程加载场景 unity多线程限制_unity_03

 

unity多线程加载场景 unity多线程限制_unity多线程加载场景_04

 

unity多线程加载场景 unity多线程限制_多线程_05

 二.什么是协程?

协程全称协同程序,不同于多线程,它不是一个线程,依附于Unity主线程。

  • 新开一个线程是独立的一个管道,和主线程并行执行;
  • 新开一个协程是在原线程之上开启,进行逻辑分时分步执行;

主要作用:

        协程适合处理需要分阶段执行的操作,如动画、延迟执行等。协程可以让程序在一定时间后再次执行,而不会阻塞主线程。这样可以避免线程切换带来的开销,提高程序性能。

        也就是说,协程是把可能会让主线程卡顿的耗时操作分时分步进行。

主要适用场景:

        异步加载文件、异步下载文件、场景异步加载、批量创建时防止卡顿。


        协程可以将一个函数分成多个部分,在每个部分执行完后暂停,等待下一次唤醒继续执行。如果不唤醒,这个协程就会被挂起。这种方式可以有效地控制程序的执行流程,使得我们可以更加灵活地控制程序的运行逻辑。

三.协程的使用

首先,要想使用协程,类要继承MonoBehavior;

在Unity中,我们可以使用Coroutine类来创建协程。通常情况下,我们会将协程定义为一个函数(使用IEnumerator关键字),然后通过StartCoroutine方法启动它。

示例:

unity多线程加载场景 unity多线程限制_主线程_06

        在这个例子中,我们定义了一个名为MyCoroutine的协程函数,该函数使用yield语句分别暂停1秒和2秒,并在每次暂停后输出一条调试信息。然后,在Start函数中,我们通过StartCoroutine方法启动了这个协程。

        需要注意的是,协程函数必须返回IEnumerator类型,而不是void类型。协程函数中使用yield语句来暂停执行,并返回一个对象,告诉协程系统应该何时继续执行。


        协程可以通过传递参数来控制它们的行为。我们可以将参数作为协程函数的参数,并在启动协程时传递给它。

 示例:

unity多线程加载场景 unity多线程限制_游戏引擎_07

        在这个例子中,我们定义了一个名为MyCoroutine的协程函数,该函数接受两个参数:一个字符串和一个浮点数。然后,在Start函数中,我们通过StartCoroutine方法启动了这个协程,并将"Hello"和3分别作为参数传递给它。

        需要注意的是,在协程函数内部,我们可以像普通函数一样使用传递进来的参数。但是,在协程函数之外,我们不能直接访问协程内部的变量。如果需要在协程之间共享数据,可以考虑使用静态变量或其他线程安全的机制。

不同的yield return

  1. yield return null:暂停协程一帧,然后继续执行下一帧, 在Update和LateUpdate之间执行;
  2. yield return new WaitForSeconds(float seconds):暂停协程指定时间后继续执行,在Update和LateUpdate之间执行;
  3. yield return new WaitForEndOfFrame():暂停协程直到当前帧渲染完毕后(摄像机和GUI)继续执行,在LateUpdate之后的渲染相关处理之后执行;
  4. yield return new WaitForFixedUpdate():暂停协程直到下一次FixedUpdate后继续执行,在FixUpdate和碰撞检测相关函数之后执行;
  5. yield return StartCoroutine(coroutine):暂停当前协程并启动一个新的协程,直到新的协程执行完成后继续执行当前协程。
  6. yield break:结束当前协程的执行。

 四.协程受对象和组件失活销毁的影响

当我们开启了一个协程,如果

  • 挂载此脚本的组件和物体销毁,协程将不会执行;
  • 物体失活协程不执行;
  • 组件失活协程执行。

五.协程的原理 

协程的本质:

        协程的本质是利用迭代器(Iterator)来实现的。在C#中,迭代器是一种特殊的对象,它可以在循环中依次返回集合中的元素。在协程中,yield return语句返回的也是一个迭代器对象,当协程执行到yield return语句时,会将执行权交给调用者,并返回一个迭代器对象,等待调用者再次调用协程来继续执行。这样就实现了协程的暂停和继续执行功能。

        所以在脚本中调用协程相当于是把一个协程函数(迭代器)放入Unity的协程调度器(StartCoroutine()函数)中帮助我们管理进行执行,具体的yield return 后面的规则,也是Unity定义的一些规则。

        我们通过获取IEnumerator对象手动模拟协程的管理;

定义一个协程函数,无法单独调用:

unity多线程加载场景 unity多线程限制_主线程_08

 获取这个协程对象:

unity多线程加载场景 unity多线程限制_unity多线程加载场景_09

      IEnumerator内部方法,属性如下:

unity多线程加载场景 unity多线程限制_unity_10


执行MoveNext(),控制台会输出“test 1”,执行了一行语句;

执行Reset(),控制台会输出 “1”, 打印的是yield return 的返回值;

如果打印Current,控制台会输出yield return的参数值;

例如这种写法,Reset会输出“123 Vector3”,Current会输出“123 (1,2,3)”;

unity多线程加载场景 unity多线程限制_unity_11


 另一个问题来了,我们可以用MoveNext()和Current来一步步执行协程函数。但是如果协程函数中有n个yield return 怎么办?写n个MoveNext()和Current吗?

 注意:MoveNext()返回bool值,当协程函数内还有可以执行的内容时返回true,没有返回flase。

unity多线程加载场景 unity多线程限制_多线程_12

使用while输出执行协程函数中的所有值:

unity多线程加载场景 unity多线程限制_多线程_13

总结:

        协程的本质就是利用C#的迭代器函数“分步执行”的特点加上协程调度逻辑实现的一套分时执行函数的规则。