6.UniTask异步委托

6.异步委托


6.1 知识点

UniTask 异步委托详解

在 Unity 开发中,异步操作是常见需求。UniTask 为异步委托提供了多种封装方式,如 UniTask.Void、UniTask.Defer、UniTask.Preserve 和 UniTask.Lazy。这些方法帮助开发者实现延迟执行、单次执行、多次 await 等需求,避免了重复 await 同一 UniTask 导致的异常问题。

使用 UniTask.Void 启动异步操作

UniTask.Void 用于直接启动异步操作,不需要等待其完成,适用于不关心返回值和任务完成状态的场景。在下面的示例中,我们利用 UniTask.Void 启动一个异步委托,并在下一帧执行后输出日志。

void TestUniTaskVoid()
{
    // UniTask.Void 接收一个异步委托并立即执行,忽略返回值
    UniTask.Void(async () =>
    {
        Debug.Log($"[UniTask.Void] 开始,当前帧: {Time.frameCount}");
        // 等待下一帧
        await UniTask.NextFrame();
        Debug.Log($"[UniTask.Void] 结束,当前帧: {Time.frameCount}");
    });
}

使用 UniTask.Defer 延迟执行异步操作

UniTask.Defer 可用于延迟创建和执行异步操作。返回的 UniTask 只有在 await 时才会执行委托中的代码。下例中,我们首先创建一个延迟任务,然后等待下一帧,最后 await 执行任务。

async UniTask TestUniTaskDefer()
{
Debug.Log($"[TestUniTaskDefer] 开始,当前帧: {Time.frameCount}");

var deferTask = UniTask.Defer(async () =>
{
    Debug.Log($"[UniTask.Defer] 开始,当前帧: {Time.frameCount}");
    // 等待下一帧
    await UniTask.NextFrame();
    Debug.Log($"[UniTask.Defer] 结束,当前帧: {Time.frameCount}");
});

await UniTask.NextFrame();

Debug.Log($"[await deferTask] 开始,当前帧: {Time.frameCount}");

await deferTask;
}

UniTask 实例的多次 await 问题

直接多次await同一 UniTask

直接多次 await 同一 UniTask 会抛出异常,因为 UniTask 实例默认只允许 await 一次。为解决这一问题,可以使用 Preserve 或 Lazy 进行封装。

async UniTask TestUniTaskAwait()
{
    // 直接多次 await UniTask 会抛出异常,因为 UniTask 实例不允许被 await 多次
    // InvalidOperationException: Token version is not matched, can not await twice or get Status after await.
    // 解决方法:使用 UniTask.Lazy 或.Preserve()
    UniTask task = UniTask.Delay(1000);
    await task;
    await task;
}

使用 Preserve 支持多次 await

调用 .Preserve() 方法后,可以对同一个异步操作进行多次 await,但是改任务只会执行一次,执行完了会缓存起来,下次await直接返回。下面的示例展示了如何对一个延迟 1 秒的 UniTask 进行多次 await 而不会抛出异常。

async UniTask TestUniTaskPreserve()
{
    // 普通 UniTask 无法被 await 多次,调用 .Preserve() 后可以多次 await。
    // 注意:异步操作都只会执行一次
    var preservedTask = UniTask.Delay(1000).Preserve();
    Debug.Log($"[UniTask.Preserve] 第一次 await 开始,当前时间: {Time.time}");
    await preservedTask;
    Debug.Log($"[UniTask.Preserve] 第一次 await 结束,当前时间: {Time.time}");
    await preservedTask;
    // 不会再执行异步操作,因为第一次 await 已经完成 打印出来的时间和第一次 await 结束的时间一致
    Debug.Log($"[UniTask.Preserve] 第二次 await 结束,当前时间: {Time.time}");
}

使用 Lazy 支持多次 await 同一操作

UniTask.Lazy 可确保异步操作仅执行一次,无论对返回的任务进行多少次 await,内部的异步逻辑都只会执行一次。下例中,通过 UniTask.Lazy 创建一个延迟任务,虽然进行了两次 await,但实际执行仅一次。

async UniTask TestUniTaskLazy()
{
    // 创建一个 lazy 异步任务,该任务可被多次 await
    var lazyTask = UniTask.Lazy(async () =>
    {
        Debug.Log($"[UniTask.Lazy] 开始,当前帧: {Time.frameCount}");
        // 模拟延时 1 秒
        await UniTask.Delay(1000);
        Debug.Log($"[UniTask.Lazy] 结束,当前帧: {Time.frameCount}");
    });

    // 第一次 await
    await lazyTask;
    // 第二次 await(由于 lazy 机制,可重复 await,不会重复执行内部逻辑)
    await lazyTask;
    Debug.Log("[UniTask.Lazy] 多次 await 完成");
}

进行测试

using UnityEngine;
using Cysharp.Threading.Tasks;

/// <summary>
/// 测试异步委托生成 UniTask 及相关封装(UniTask.Void、UniTask.Defer、UniTask.Lazy/.Preserve)的示例。
/// 注意:UniTask 实例不允许被 await 多次,若需要多次 await,请使用 UniTask.Lazy 或 .Preserve()。
/// </summary>
public class Lesson06_异步委托 : MonoBehaviour
{
    async void Start()
    {
        // 示例一:使用 UniTask.Void 直接启动异步委托,不需要 await
        TestUniTaskVoid();

        // 示例二:使用 UniTask.Defer 创建异步委托,返回的 UniTask 需要 await 才会执行
        await TestUniTaskDefer();

        // 示例三:使用 Preserve 或 UniTask.Lazy 支持多次 await 同一异步操作
        // await TestUniTaskAwait();
        await TestUniTaskPreserve();
        await TestUniTaskLazy();
    }

    /// <summary>
    /// 使用 UniTask.Void 启动异步操作,不需要等待其完成。
    /// 适用于不需要返回值且不关心任务完成状态的异步操作。
    /// </summary>
    void TestUniTaskVoid()
    {
        // UniTask.Void 接收一个异步委托并立即执行,忽略返回值
        UniTask.Void(async () =>
        {
            Debug.Log($"[UniTask.Void] 开始,当前帧: {Time.frameCount}");
            // 等待下一帧
            await UniTask.NextFrame();
            Debug.Log($"[UniTask.Void] 结束,当前帧: {Time.frameCount}");
        });
    }

    /// <summary>
    /// 使用 UniTask.Defer 创建一个异步委托,返回 UniTask,必须 await 才能执行。
    /// 用于延迟创建和执行异步操作,只有在实际 await 时才会执行委托中的代码。
    /// </summary>
    async UniTask TestUniTaskDefer()
    {
        Debug.Log($"[TestUniTaskDefer] 开始,当前帧: {Time.frameCount}");

        var deferTask = UniTask.Defer(async () =>
        {
            Debug.Log($"[UniTask.Defer] 开始,当前帧: {Time.frameCount}");
            // 等待下一帧
            await UniTask.NextFrame();
            Debug.Log($"[UniTask.Defer] 结束,当前帧: {Time.frameCount}");
        });

        await UniTask.NextFrame();

        Debug.Log($"[await deferTask] 开始,当前帧: {Time.frameCount}");

        await deferTask;
    }

    /// <summary>
    /// 直接多次 await UniTask,会抛出异常。
    /// </summary>
    async UniTask TestUniTaskAwait()
    {
        // 直接多次 await UniTask 会抛出异常,因为 UniTask 实例不允许被 await 多次
        // InvalidOperationException: Token version is not matched, can not await twice or get Status after await.
        // 解决方法:使用 UniTask.Lazy 或.Preserve()
        UniTask task = UniTask.Delay(1000);
        await task;
        await task;
    }

    /// <summary>
    /// 使用 Preserve 方法包装 UniTask,允许对同一个异步操作进行多次 await。
    /// </summary>
    async UniTask TestUniTaskPreserve()
    {
        // 普通 UniTask 无法被 await 多次,调用 .Preserve() 后可以多次 await。
        // 注意:异步操作都只会执行一次
        var preservedTask = UniTask.Delay(1000).Preserve();
        Debug.Log($"[UniTask.Preserve] 第一次 await 开始,当前时间: {Time.time}");
        await preservedTask;
        Debug.Log($"[UniTask.Preserve] 第一次 await 结束,当前时间: {Time.time}");
        await preservedTask;
        // 不会再执行异步操作,因为第一次 await 已经完成 打印出来的时间和第一次 await 结束的时间一致
        Debug.Log($"[UniTask.Preserve] 第二次 await 结束,当前时间: {Time.time}");
    }

    /// <summary>
    /// 使用 UniTask.Lazy 支持多次 await 一个延迟操作。
    /// 意义:
    /// 确保异步操作仅执行一次:无论对返回的任务进行多少次 await,内部的异步操作都只会执行一次。这对于需要确保某些初始化逻辑或资源加载过程只执行一次的场景非常有用。
    /// 支持多次 await 同一任务:在某些情况下,可能需要在不同的地方等待同一个异步操作的完成。使用 UniTask.Lazy,可以安全地对同一个任务进行多次 await,而不会引发异常。
    /// </summary>
    async UniTask TestUniTaskLazy()
    {
        // 创建一个 lazy 异步任务,该任务可被多次 await
        var lazyTask = UniTask.Lazy(async () =>
        {
            Debug.Log($"[UniTask.Lazy] 开始,当前帧: {Time.frameCount}");
            // 模拟延时 1 秒
            await UniTask.Delay(1000);
            Debug.Log($"[UniTask.Lazy] 结束,当前帧: {Time.frameCount}");
        });

        // 第一次 await
        await lazyTask;
        // 第二次 await(由于 lazy 机制,可重复 await,不会重复执行内部逻辑)
        await lazyTask;
        Debug.Log("[UniTask.Lazy] 多次 await 完成");
    }
}

总结

本文详细介绍了 UniTask 在异步委托场景下的应用。通过 UniTask.Void 可以直接启动异步操作;利用 UniTask.Defer 延迟创建和执行异步代码;而使用 Preserve 和 Lazy 则能解决同一 UniTask 多次 await 的问题。合理选择和使用这些方法,可以使异步逻辑更加简洁和高效,提升 Unity 开发体验。


6.2 知识点代码

Lesson06_异步委托.cs

using UnityEngine;
using Cysharp.Threading.Tasks;

/// <summary>
/// 测试异步委托生成 UniTask 及相关封装(UniTask.Void、UniTask.Defer、UniTask.Lazy/.Preserve)的示例。
/// 注意:UniTask 实例不允许被 await 多次,若需要多次 await,请使用 UniTask.Lazy 或 .Preserve()。
/// </summary>
public class Lesson06_异步委托 : MonoBehaviour
{
    async void Start()
    {
        // 示例一:使用 UniTask.Void 直接启动异步委托,不需要 await
        TestUniTaskVoid();

        // 示例二:使用 UniTask.Defer 创建异步委托,返回的 UniTask 需要 await 才会执行
        await TestUniTaskDefer();

        // 示例三:使用 Preserve 或 UniTask.Lazy 支持多次 await 同一异步操作
        // await TestUniTaskAwait();
        await TestUniTaskPreserve();
        await TestUniTaskLazy();
    }

    /// <summary>
    /// 使用 UniTask.Void 启动异步操作,不需要等待其完成。
    /// 适用于不需要返回值且不关心任务完成状态的异步操作。
    /// </summary>
    void TestUniTaskVoid()
    {
        // UniTask.Void 接收一个异步委托并立即执行,忽略返回值
        UniTask.Void(async () =>
        {
            Debug.Log($"[UniTask.Void] 开始,当前帧: {Time.frameCount}");
            // 等待下一帧
            await UniTask.NextFrame();
            Debug.Log($"[UniTask.Void] 结束,当前帧: {Time.frameCount}");
        });
    }

    /// <summary>
    /// 使用 UniTask.Defer 创建一个异步委托,返回 UniTask,必须 await 才能执行。
    /// 用于延迟创建和执行异步操作,只有在实际 await 时才会执行委托中的代码。
    /// </summary>
    async UniTask TestUniTaskDefer()
    {
        Debug.Log($"[TestUniTaskDefer] 开始,当前帧: {Time.frameCount}");

        var deferTask = UniTask.Defer(async () =>
        {
            Debug.Log($"[UniTask.Defer] 开始,当前帧: {Time.frameCount}");
            // 等待下一帧
            await UniTask.NextFrame();
            Debug.Log($"[UniTask.Defer] 结束,当前帧: {Time.frameCount}");
        });

        await UniTask.NextFrame();

        Debug.Log($"[await deferTask] 开始,当前帧: {Time.frameCount}");

        await deferTask;
    }

    /// <summary>
    /// 直接多次 await UniTask,会抛出异常。
    /// </summary>
    async UniTask TestUniTaskAwait()
    {
        // 直接多次 await UniTask 会抛出异常,因为 UniTask 实例不允许被 await 多次
        // InvalidOperationException: Token version is not matched, can not await twice or get Status after await.
        // 解决方法:使用 UniTask.Lazy 或.Preserve()
        UniTask task = UniTask.Delay(1000);
        await task;
        await task;
    }

    /// <summary>
    /// 使用 Preserve 方法包装 UniTask,允许对同一个异步操作进行多次 await。
    /// </summary>
    async UniTask TestUniTaskPreserve()
    {
        // 普通 UniTask 无法被 await 多次,调用 .Preserve() 后可以多次 await。
        // 注意:异步操作都只会执行一次
        var preservedTask = UniTask.Delay(1000).Preserve();
        Debug.Log($"[UniTask.Preserve] 第一次 await 开始,当前时间: {Time.time}");
        await preservedTask;
        Debug.Log($"[UniTask.Preserve] 第一次 await 结束,当前时间: {Time.time}");
        await preservedTask;
        // 不会再执行异步操作,因为第一次 await 已经完成 打印出来的时间和第一次 await 结束的时间一致
        Debug.Log($"[UniTask.Preserve] 第二次 await 结束,当前时间: {Time.time}");
    }

    /// <summary>
    /// 使用 UniTask.Lazy 支持多次 await 一个延迟操作。
    /// 意义:
    /// 确保异步操作仅执行一次:无论对返回的任务进行多少次 await,内部的异步操作都只会执行一次。这对于需要确保某些初始化逻辑或资源加载过程只执行一次的场景非常有用。
    /// 支持多次 await 同一任务:在某些情况下,可能需要在不同的地方等待同一个异步操作的完成。使用 UniTask.Lazy,可以安全地对同一个任务进行多次 await,而不会引发异常。
    /// </summary>
    async UniTask TestUniTaskLazy()
    {
        // 创建一个 lazy 异步任务,该任务可被多次 await
        var lazyTask = UniTask.Lazy(async () =>
        {
            Debug.Log($"[UniTask.Lazy] 开始,当前帧: {Time.frameCount}");
            // 模拟延时 1 秒
            await UniTask.Delay(1000);
            Debug.Log($"[UniTask.Lazy] 结束,当前帧: {Time.frameCount}");
        });

        // 第一次 await
        await lazyTask;
        // 第二次 await(由于 lazy 机制,可重复 await,不会重复执行内部逻辑)
        await lazyTask;
        Debug.Log("[UniTask.Lazy] 多次 await 完成");
    }
}


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

×

喜欢就点赞,疼爱就打赏