2.UniTask对比协程和Task

2.协程和Task


2.1 知识点

同步与异步的基本概念

  • 同步
    同步调用意味着方法1调用方法2时,必须等待方法2执行完毕后才能继续执行方法1。这样的调用模式虽然简单,但容易导致阻塞,降低程序响应速度。

  • 异步
    异步调用允许方法1与方法2在代码中同时或交替执行。调用后方法1不会等待方法2执行完成,而是立即执行后续代码。这样可以提高程序响应性,充分利用资源,尤其在处理耗时操作时尤为有效。

异步不等于多线程

需要注意的是,异步编程仅是解决阻塞问题的编程模型,它并不意味着一定会使用多线程:

  • 在 Unity 中,协程通过单线程的 PlayerLoop 实现异步效果,并没有真正创建新的线程。
  • C# 的 Task 有时会涉及线程池切换,但其设计初衷主要是为了异步等待,而非并发处理。

因此,异步的优势在于不阻塞当前线程,而多线程则涉及额外的线程管理与同步问题。

Unity 协程存在的问题

传统的 Unity 协程虽然简单易用,但也存在一些局限性:

  • 依赖 MonoBehaviour
    协程只能运行在继承了 MonoBehaviour 的组件上,无法在无此组件的上下文中使用。

  • 异常处理缺失
    协程中异常往往无法被捕获或传递,导致调试与错误处理变得困难。

  • 返回值获取困难
    协程无法像普通函数那样直接返回值,需要额外机制来获取结果,增加了编码复杂性。

C# 原生 Task 的局限性

虽然 C# 的 Task 为异步编程提供了一定支持,但在 Unity 中使用时也存在一些不足:

  • 资源消耗较高
    Task 由于涉及线程切换和上下文捕获,可能会带来额外的内存分配和性能开销。

  • 跨线程处理复杂
    Task 常常需要手动管理线程同步和上下文切换,与 Unity 的单线程模型不完全兼容,增加了开发和调试的难度。

测试脚本示例

以下示例代码展示了使用 Unity 协程和 C# Task 进行异步操作的效果,帮助理解异步调用不会阻塞主线程:

using System.Collections;
using System.Threading.Tasks;
using UnityEngine;

public class Lesson02_协程和Task : MonoBehaviour
{
    private bool _coroutineEnd = false;
    private bool _taskEnd = false;

    async void Start()
    {
        Debug.LogWarning("Start :" + Time.time);
        StartCoroutine(CoroutineTest());
        await TaskTest();
    }

    IEnumerator CoroutineTest()
    {
        Debug.LogWarning("Coroutine Start:" + Time.time);
        yield return new WaitForSeconds(1);
        Debug.LogWarning("Coroutine End:" + Time.time);
        _coroutineEnd = true;
    }

    async Task<int> TaskTest()
    {
        Debug.LogWarning("Task Start:" + Time.time);
        await Task.Delay(1000);
        Debug.LogWarning("Task End:" + Time.time);
        _taskEnd = true;
        return 1;
    }

    void Update()
    {
        if (_coroutineEnd 22 _taskEnd)
        {
            return;
        }
        Debug.Log("Update执行了" + Time.time);
    }
}

在此脚本中:

  • 协程通过 StartCoroutine 启动并在等待 1 秒后结束。
  • Task 异步方法使用 await Task.Delay(1000) 实现等待 1 秒,然后结束。但实际上可能并不准确。
  • Update 方法中,可以看到主线程持续运行,不论协程或 Task 是否完成,验证了异步调用不会阻塞主线程。

UniTask 的必要性与优势

为了解决上述问题,UniTask 专为 Unity 开发设计,其优势包括:

  • 零 GC 分配
    采用基于值类型的 UniTask<T> 和自定义 AsyncMethodBuilder,大幅降低内存分配与 GC 压力。

  • 单线程高效运行
    完全基于 Unity 的 PlayerLoop 运行,无需依赖线程池,避免了不必要的线程切换和同步复杂性。

  • 直接支持 Unity 异步对象
    能够直接对 Unity 的 AsyncOperation、协程等进行 await,简化异步代码的编写。

  • 兼容性与易用性
    UniTask 保留了 C# 原生 Task/ValueTask 的语义,同时在接口上进行了扩展,使异步编程更加直观和高效,极大地提升了开发效率。

总结

  • 同步调用要求前一个方法执行完毕后才能继续,容易造成阻塞。
  • 异步调用允许方法之间并行或交替执行,显著提高了响应性和资源利用率。
  • 传统 Unity 协程存在依赖 MonoBehaviour、异常处理和返回值获取等问题。
  • C# Task 虽支持异步,但在 Unity 中可能因资源消耗和线程切换复杂性而影响性能。
  • UniTask 针对 Unity 环境进行了专门优化,实现零 GC、无多线程切换开销,简化了异步操作的使用,使开发者能够以更直观高效的方式编写异步代码。

2.2 知识点代码

Lesson02_协程和Task.cs

using System.Collections;
using System.Threading.Tasks;
using UnityEngine;


public class Lesson02_协程和Task : MonoBehaviour
{
    private bool _coroutineEnd = false;
    private bool _taskEnd = false;

    async void Start()
    {
        Debug.LogWarning("Start :" + Time.time);
        StartCoroutine(CoroutineTest());
        await TaskTest();
    }

    IEnumerator CoroutineTest()
    {
        Debug.LogWarning("Coroutine Start:" + Time.time);
        yield return new WaitForSeconds(1);
        Debug.LogWarning("Coroutine End:" + Time.time);
        _coroutineEnd = true;
    }

    async Task<int> TaskTest()
    {
        Debug.LogWarning("Task Start:" + Time.time);
        await Task.Delay(1000);
        Debug.LogWarning("Task End:" + Time.time);
        _taskEnd = true;
        return 1;
    }

    void Update()
    {
        if (_coroutineEnd && _taskEnd)
        {
            return;
        }

        Debug.Log("Update执行了" + Time.time);
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏