Unity协程Coroutine与UniTask对比

发布于:2025-06-07 ⋅ 阅读:(15) ⋅ 点赞:(0)

原理对比

Coroutine UniTask
本质 IEnumerator 的协作调度器 async/await 状态机(IAsyncStateMachine)
调度方式 Unity 内部调用 MoveNext() 自建 PlayerLoopRunner 控制状态推进
内存管理 引用类型,频繁分配 GC 结构体 UniTask,低 GC 压力
多线程支持 主线程限制 可结合多线程(但默认仍在主线程)
工具组合能力 强(如 WhenAll, WithCancellation)

调用方式

协程

1.不使用StartCoroutine调用时,可通过编译,但是无法启动,协程进不去

2.使用StartCoroutine可正确执行协程逻辑,正常执行等待,对当前函数体无要求

3.可使用yield return等待协程执行完毕,需要当前函数体有IEnumerator关键字标识

4.可使用await关键字等待协程执行完毕,需要当前函数体有async关键字标识

示例代码如下

void CorTest()
{
    Test();
    Debug.Log("Coroutine 3");
    StartCoroutine(Test());
    Debug.Log("Coroutine 4");
}

IEnumerator Test()
{
    Debug.Log("Coroutine 1");
    yield return new WaitForSeconds(1.1f);
    Debug.Log("Coroutine 2");
}

当调用CorTest后,输出结果为

可以看到,先输出了"Coroutine 3",而没有输出"Coroutine 1",表示没有使用StartCoroutine启动时,协程是进不去的。使用了StartCoroutine后,"Coroutine 2"在"Coroutine 1"及"Coroutine 4"一秒后输出。await关键字情况同下面UniTask调用

UniTask

1.UniTask无论是否使用await关键字,都可以正确进入逻辑,正常执行等待,对当前函数体无要求

2.可使用await等待UniTask执行完毕,需要当前函数体有async关键字标识

示例代码如下

async void TaskTest()
{
    Test2();
    Debug.Log("UniTask 3");
    await Test2();
    Debug.Log("UniTask 4");
}

async UniTask Test2()
{
    Debug.Log("UniTask 1");
    await UniTask.Delay(1100);
    Debug.Log("UniTask 2");
}

当调用TaskTest后,输出结果为

可以看到,两次调用Test2均正常进入,且正常执行了等待逻辑,两次"UniTask 2"输出均在"UniTask 1"后,而"UniTask 4"输出也是在"UniTask 2"后执行的。函数体如果没有async关键字时,内部是无法使用await的,编译不通过。

性能测试

yield return null  VS UniTask.Yield()

测试代码

public int times = 1000;

void CorProTest()
{
    StartCoroutine(CorProEnum());
}

IEnumerator CorProEnum()
{
    for (int i = 0; i < times; i++)
    {
        yield return null;
    }
}

void UniTaskProTest()
{
    UniTaskProTask();
}

async UniTask UniTaskProTask()
{
    for (int i = 0; i < times; i++)
    {
        await UniTask.Yield();
    }
}

由于无法抓取一段时间内的纯Profiler数据,所以只取一帧的数据,每帧数据都是一致的。

可以看到,两个对GC都没有影响,因为协程本身并没有新建对象,所以不存在分配内存。可以理解成等价的。

 yield return new WaitForSeconds VS UniTask.Delay

测试代码

public int times = 1000;

void CorProTest()
{
    StartCoroutine(CorProEnum());
}

IEnumerator CorProEnum()
{
    for (int i = 0; i < times; i++)
    {
        yield return new WaitForSeconds(0.01f);
    }
}

void UniTaskProTest()
{
    UniTaskProTask();
}

async UniTask UniTaskProTask()
{
    for (int i = 0; i < times; i++)
    {
        await UniTask.Delay(10);
    }
}

同上,抓取某一帧的数据

 可以看到,调用yield return new WaitForSeconds(0.01f)时,有20B的内存分配,这是因为创建了引用对象WaitForSeconds,所以必定会有内存分配。调用await UniTask.Delay(10)没有内存分配,是因为UniTask内部使用的是结构体,而不是类。

yield return new WaitUntil VS UniTask.WaitUntil

测试代码

public int times = 1000;

void CorProTest()
{
    StartCoroutine(CorProEnum());
}

IEnumerator CorProEnum()
{
    bool value = true;
    for (int i = 0; i < times; i++)
    {
        yield return new WaitUntil(() => value);
    }
}

void UniTaskProTest()
{
    UniTaskProTask();
}

async UniTask UniTaskProTask()
{
    bool value = true;
    for (int i = 0; i < times; i++)
    {
        await UniTask.WaitUntil(() => value);
    }
}

同上,抓取某一帧数据

可以看到 ,调用yield return new WaitUntil时,有24B的内存分配,这是因为创建了引用对象WaitUntil,所以必定会有内存分配。调用await UniTask.WaitUntil没有内存分配,是因为UniTask内部使用的是结构体,而不是类。

总结

        测试了这三种常用的用法,可以看到,协程除了null没有GC产生(因为没有创建对象)外,其他两种用户均产生了GC,只是量比较小,而UniTask三种用法都没有GC产生。

        如果只考虑GC方面的差异,在项目使用过程中,如果量比较大,使用比较频繁,建议使用UniTask。而对于一般用量来讲,差距可以忽略不计。而GC是可以使用对象池来优化的,可以一定程度上降低GC的分配。对象池参考另一篇博客从CPU缓存出发对引用池进行优化

        然而,最终选择使用哪一种,需要结合其他情况考虑,协程使用起来比较方便,而UniTask也有一些比较好的功能,比如UniTask支持带返回值的异步,封装了多任务同时进行、等待以及其他功能。


网站公告

今日签到

点亮在社区的每一天
去签到