简单了解Unity中的异步实现差异

Posted by ELITTK on 2021-10-30

协程与任务是什么

协程Coroutine

协程不是线程
协程可以理解为通过把控制权返还给Unity的方式来实现跨帧执行的一个方便的函数。
例如许多需要定期执行的任务,与其放在Update函数之中每帧执行,不如使用协程的方式既能提高可读性也能优化性能。

必须注意的是,协程不是线程。在协程内运行的同步操作仍然在主线程上执行。如果需要减少主线程上花费的 CPU 时间,与任何其他脚本代码中一样,在协程中避免阻塞操作同样很重要。

——Unity Documentation

协程的异步操作由Unity提供的StartCoroutine()以及yield()来实现,Unity中协程是在每次Update之后所处理的功能,由yield()来负责挂起并把控制权交给Unity,并在达到条件后再在Update之后重新唤醒。具体执行流程可参考下方的流程图

MonoBehaviour流程图

任务Task

任务是C#中多线程中的一个内容,需要Using System.Threading.Task

有别于单纯的使用Thread开辟线程,Task是对象,是通过任务调度器与线程池来判断进行最高效的做法(默认为从线程池请求一个工作者线程)。Task本身可以通过Task.Run()的方式来使用,还可以使用ContinueWith()的方式来创建延续任务,还可以通过async以及await的方式来实现基于任务的异步模式(Task-based Asynchronous Pattern, TAP)

当然这只是一个大致的概括,具体实现还是推荐参考巨硬的官方教程(见下文参考资料)

协程与任务的差异

在Github上我找到了一个测试https://github.com/JoaoBorks/unity-async-vs-coroutine下面是测试在我自己电脑上的运行结果。
时间
内存
垃圾回收内存
堆外分配内存出发的垃圾回收

各位读者可能会有疑问,这个UniTask究竟是什么?这个留到稍后再讲。

在上面的数据中,我们会发现协程与任务相比,TAP模式消耗的性能更多且更不稳定,而且相较于协程对于gameObjct的支持更弱。但这并不代表使用任务更弱,任务系统有更为高级的错误处理系统以及更为完善的析构系统,同时若是要尝试更为高级的线程同步(如System.Threading.Mutex等等)等功能,使用任务也会方便不少。

然后,就诞生了神奇的UniTask,正如读者所见,他需要的垃圾回收是0

UniTask

UniTask是一个同时集成了上文所说的任务与协程优点的产物,既有上文提到的任务的强大功能,又有像协程一样对Unity良好的兼容性。在package里面导入它的Git链接即可使用到Unity当中。有兴趣的读者可以看看它的官方Github(见下文参考资料)。

参考资料

  1. Unity文档中的协程[https://docs.unity3d.com/cn/current/Manual/BestPracticeUnderstandingPerformanceInUnity3.html]
  2. Unity文档中的事件执行流程[https://docs.unity3d.com/Manual/ExecutionOrder.html]
  3. 微软官方手册中的TAP[https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/task-based-asynchronous-programming]
  4. UniTask的官方Blog[https://neuecc.medium.com/unitask-v2-zero-allocation-async-await-for-unity-with-asynchronous-linq-1aa9c96aa7dd]