协程的执行方式与其他脚本代码不同。大多数脚本代码只显示在性能跟踪内的位于特定 Unity 回调调用下某一个位置。但是,协程的 CPU 代码总是出现在跟踪内的两个位置。
协程中的所有初始代码(从协程方法的开始一直到第一次暂停)将出现在跟踪过程中任何启动协程的位置。通常出现在调用 StartCoroutine
方法的位置。从 Unity 回调(例如返回 IEnumerator
的 Start
回调)生成的协程首先出现在各自的 Unity 回调中。
协程代码的所有其余部分(从第一次恢复一直到完成执行)将显示在 Unity 主循环内出现的 DelayedCallManager
行中。
To understand why this occurs, consider how a coroutine actually is executed.
Coroutines are backed by an instance of a class that is autogenerated by the C# compiler. This object is needed to track the state of the coroutine across multiple invocations of what is, to the programmer, a single method. Because local-scope variables within the coroutine must persist across yield
calls, those local-scope variables are hoisted into the generated class and therefore remain allocated on the heap for the duration of the coroutine. This object also tracks the internal state of the coroutine: it remembers at what point in the code the coroutine must be resume after yielding.
因此,启动协程引起的内存压力等于固定开销成本加上其局部变量的消耗。
启动协程的代码将构造并调用此对象,然后 Unity 的 DelayedCallManager
在每当满足协程的暂停条件时再次调用此对象。由于协程通常在其他协程之外启动,因此它们的执行成本将分担到上述两个位置。
在上面的截屏中可以看到这种情况,其中的 DelayedCallManager
正在恢复几个不同的协程:PopulateCharacters
、AsyncLoad
和 LoadDatabase
是其中需要注意的协程。
尽可能将一系列操作压缩到最少数量的协程中。虽然嵌套的协程非常有利于确保代码的条理性和进行维护,但协程跟踪对象本身会导致产生更高的内存开销。
如果一个协程几乎每帧都运行并且在长时间运行操作中不会暂停,那么用 Update
或 LateUpdate
回调来替换该协程通常更合理一些。例如长时间运行或无限循环的协程。
必须注意的是,协程不是线程。在协程内运行的同步操作仍然在主线程上执行。如果需要减少主线程上花费的 CPU 时间,与任何其他脚本代码中一样,在协程中避免阻塞操作同样很重要。
在处理长时间异步操作(例如等待 HTTP 传输、资源加载或文件 I/O 完成)时,最适合使用协程。