首发于 UniRx/UniTask

UniTask中文使用指南(一)

简介

UniTask的特点

  • 为 Unity 提供有效的无GC async/await集成。
  • 基于Struct `UniTask<T>` 的自定义 AsyncMethodBuilder,实现零GC,使所有Unity的异步操作和协程可以await
  • 基于PlayerLoop的Task( `UniTask.Yield`、 `UniTask.Delay`、 `UniTask.DelayFrame` 等)这使得能够替换所有协程操作
  • MonoBehaviour 消息事件和 uGUI 事件为可使用Await/AsyncEnumerable
  • 完全在 Unity 的 PlayerLoop 上运行,因此不使用线程,可在 WebGL、wasm 等平台上运行。
  • 异步 LINQ,具有Channel和 AsyncReactiveProperty
  • 防止内存泄漏的 TaskTracker (Task追踪器)窗口
  • 与Task/ValueTask/IValueTaskSource 的行为高度兼容

为什么需要 UniTask(自定义类似Task对象)?

因为原生 Task 太重,与 Unity 线程(单线程)不匹配。UniTask 不使用线程和SynchronizationContext/ExecutionContext,因为 Unity 的异步对象由 Unity 的引擎层自动调度。它实现了更快和更低的分配,并且与Unity完全集成。


插件使用要求

UniTask 功能依赖于 C# 7.0( 类似Task的自定义异步方法生成器特性 ),所以需要的Unity版本是在Unity 2018.3之后,官方支持的最低版本是Unity 2018.4.13f1。


语法入门

使用UniTask所需的命名空间 using Cysharp.Threading.Tasks;

您可以将类型返回为 struct UniTask<T>(或 UniTask),它是 Task<T> 的Unity专用轻量级替代方案,

//实现0开销(0GC和快速执行)的async/await Unity集成
async UniTask<string> DemoAsync()
 //您可以await Unity async对象
 var asset = await Resources.LoadAsync<TextAsset>("foo");
 var txt = (await UnityWebRequest.Get("https://...").SendWebRequest()).downloadHandler.text;
 await SceneManager.LoadSceneAsync("scene2");
 //.WithCancellation 启用取消方法,GetCancellationTokenOnDestroy 与 GameObject 的生命周期同步
 var asset2 = await Resources.LoadAsync<TextAsset>("bar").WithCancellation(this.GetCancellationTokenOnDestroy());
 //.ToUniTask 接受进度回调(和完整的参数),Progress.Create 是 IProgress<T> 的轻量级替代品
 var asset3 = await Resources.LoadAsync<TextAsset>("baz").ToUniTask(Progress.Create<float>(x => Debug.Log(x)));
 //像协程一样,是await基于帧的操作
 await UniTask.DelayFrame(100); 
 //替换 yield return new WaitForSeconds/WaitForSecondsRealtime
 await UniTask.Delay(TimeSpan.FromSeconds(10), ignoreTimeScale: false);
 //产生任何播放器循环时间(PreUpdate、Update、LateUpdate 等...)
 await UniTask.Yield(PlayerLoopTiming.PreLateUpdate);
 //替换 yield return null
 await UniTask.Yield();
 await UniTask.NextFrame();
 //替换 WaitForEndOfFrame(需要 MonoBehaviour(CoroutineRunner))
 await UniTask.WaitForEndOfFrame(this); //这是 MonoBehaviour
 //替换 yield return new WaitForFixedUpdate(同 UniTask.Yield(PlayerLoopTiming.FixedUpdate))
 await UniTask.WaitForFixedUpdate();
 //替换 yield return WaitUntil
 await UniTask.WaitUntil(() => isActive == false);
 //WaitUntil 的帮助方法
 await UniTask.WaitUntilValueChanged(this, x => x.isActive);
 //您可以await IEnumerator 协程
 await FooCoroutineEnumerator();
 //您可以await C#标准Task
 await Task.Run(() => 100);
 //多线程,此代码下运行在 ThreadPool 上
 await UniTask.SwitchToThreadPool();
 /*在线程池上工作*/
 //返回主线程(与 UniRx 中的 `ObserveOnMainThread` 相同)
 await UniTask.SwitchToMainThread();
 //获取async网络请求
 async UniTask<string> GetTextAsync(UnityWebRequest req)
 var op = await req.SendWebRequest();
 return op.downloadHandler.text;
 var task1 = GetTextAsync(UnityWebRequest.Get("http://google.com"));
 var task2 = GetTextAsync(UnityWebRequest.Get("http://bing.com"));
 var task3 = GetTextAsync(UnityWebRequest.Get("http://yahoo.com"));
 //并发async await并通过元组语法轻松获取结果
 var (google, bing, yahoo) = await UniTask.WhenAll(task1, task2, task3);
 //WhenAll的简写,tuple可以直接await
 var (google2, bing2, yahoo2) = await (task1, task2, task3);
 //返回async值。(或者您可以使用 `UniTask`(无结果)、`UniTaskVoid`(即发即弃))。
 return (asset as TextAsset)?.text ?? throw new InvalidOperationException("Asset not found");

UniTask入门注意事项

1.约束

这与NET Standard 2.1 中引入的[ ValueTask/I ValueTaskSource ]约束类似:

以下操作绝不应该在ValueTask实例上执行:

  • await实例多次
  • 多次调用 AsTask
  • 在操作尚未完成时使用.Result 或.GetAwaiter().GetResult(),或者多次使用它们

如果执行上述任何操作,结果都是未定义的。

错误示范:

var task = UniTask.DelayFrame(10);
await task;
await task; // 抛出异常


2.如果需要支持await多次

可以使用:

a.UniTask.Lazy 可用于延迟运行UniTask (返回值为AsyncLazy)

  • a.因为AsyncLazy是awaitable,所以可以直接await
  • b.AsyncLazy可多重await
  • c.如果想把它转换成一个UniTask,使用AsyncLazy.Task

脚本示例:

//定义
public static AsyncLazy<T> Lazy<T>(Func<UniTask<T>> factory)
//-------------------------以下为示例-------------------------------------
var asyncLazy = UniTask.Lazy(Factory);
await asyncLazy; //可以直接await
await asyncLazy.Task; //转换为 UniTask
//UniTask.Lazy 结果可以await任意次数

b.*.Preserve() (内部缓存的结果)

脚本示例:

private async UniTaskVoid DoAsync(CancellationToken token)
        var uniTask = GetAsync("Unity", token);
        // 转换成UniTask,可以用Preserve() await任意次数。
        var reusable = uniTask.Preserve();
        await reusable;
        await reusable;
    catch (InvalidOperationException e)
        Debug.LogException(e);

c.UniTaskCompletionSource (这个会在下文讲到)


3.UniTaskV2删除了UniTask.Result/IsCompleted,需要用 GetAwaiter()


4.支持Unity中的异步操作(await),需要引用 using Cysharp.Threading.Tasks;

支持的操作:

  • AsyncOperation
  • ResourceRequest
  • AssetBundleRequest
  • AssetBundleCreateRequest
  • UnityWebRequestAsyncOperation
  • AsyncGPUReadbackRequest
  • IEnumerator


5.UniTask提供了 三种模式的扩展方法

a.* await asyncOperation;

AssetBundleRequest 有 `asset` 和 `allAssets`,默认为await返回 `asset`。如果你想得到 `allAssets`,你可以使用 `AwaitForAllAssets()` 方法。


b.* .WithCancellation(CancellationToken);

`WithCancellation` 是 `ToUniTask`的简单版本,两者都会返回 `UniTask`

await 直接从 PlayerLoop 的原生生命周期返回,而 WithCancellation 和 ToUniTask 则从指定的 PlayerLoopTiming 返回


c.*.ToUniTask(IProgress,PlayerLoopTiming,CancellationToken);


延续操作(返回值元组)

1.UniTask.WhenAll (全部完成后...)

2.UniTask.WhenAny (任一完成后...)

脚本示例:

//加载完三张图片后延续
public async UniTaskVoid LoadManyAsync()
    // 并行加载.
    var (a, b, c) = await UniTask.WhenAll(
        LoadAsSprite("foo"),
        LoadAsSprite("bar"),
        LoadAsSprite("baz"));
async UniTask<Sprite> LoadAsSprite(string path)
    var resource = await Resources.LoadAsync<Sprite>(path);
    return (resource as Sprite);

转换操作

1. Task转换

  • *.AsUniTask (Task -> UniTask)(UniTask<T> -> UniTask [这两者的转换是0消耗的])
  • *.AsAsyncUnitUniTask (UniTask -> UniTask<AsyncUnit>)
  • UniTaskCompletionSource<T> Task回调转换为 UniTask


TaskCompletionSource<T>额外讲解

a.UniTaskCompletionSource<T>是原生TaskCompletionSource<T>的轻量级版本。


b.问 原生TaskCompletionSource<T>用来干啥的?

答:用来返回异步回调任务的状态

原生TaskCompletionSource脚本示例:

public Task<Args> SomeApiWrapper()
    TaskCompletionSource<Args> tcs = new TaskCompletionSource<Args>(); 
    var obj = new SomeApi();
    // 任务完成后会收到回调
    obj.Done += (args) => 
    //这将通知SomeApiWrapper的调用者,该任务刚刚完成
        tcs.SetResult(args);
    // 开始任务
    obj.Do();
    return tcs.Task;

UniTaskCompletionSource脚本示例:

public UniTask<int> WrapByUniTaskCompletionSource()
    var utcs = new UniTaskCompletionSource<int>();
    // 当完成时,调用 utcs.TrySetResult();
    // 当失败时,调用 utcs.TrySetException();
    // 当取消时,调用 utcs.TrySetCanceled();
    return utcs.Task; //返回 UniTask<int>


2. 异步转换为协程

*.ToCoroutine()


取消和异常处理

1.简述:

CancellationToken表示异步的生命周期

一些UniTask工厂方法有一个CancellationToken cancellationToken = default参数。此外,Unity的一些异步操作有WithCancellation(CancellationToken)和ToUniTask(..., CancellationToken cancellation = default)扩展方法


2.使用方法+脚本使用案例

a.标准的CancellationTokenSource ,将CancellationToken传递给参数

var cts = new CancellationTokenSource();
cancelButton.onClick.AddListener(() =>
    cts.Cancel();
await UnityWebRequest.Get("http://google.co.jp").SendWebRequest().WithCancellation(cts.Token);
await UniTask.DelayFrame(1000, cancellationToken: cts.Token);


b.传递MonoBehaviour 的扩展方法建立 ` GetCancellationTokenOnDestroy `

// 这个CancellationToken的生命周期与GameObject相同
await UniTask.DelayFrame(1000, cancellationToken: this.GetCancellationTokenOnDestroy());


c.对于传播Cancellation(取消来源),所有的异步方法都建议在最后一个参数中接受CancellationToken cancellationToken,并 将CancellationToken从根部传到末端

await FooAsync(this.GetCancellationTokenOnDestroy());
// ---
async UniTask FooAsync(CancellationToken cancellationToken)
    await BarAsync(cancellationToken);
async UniTask BarAsync(CancellationToken cancellationToken)
    await UniTask.Delay(TimeSpan.FromSeconds(3), cancellationToken);


d.WaitUntilCanceled 示例

private void Start()
    var token = this.GetCancellationTokenOnDestroy();
    WaitForCanceledAsync(token).Forget();
private async UniTaskVoid WaitForCanceledAsync(CancellationToken token)
    await token.WaitUntilCanceled();
    Debug.Log("Canceled!");

同样可以用CancellationToken.ToUniTask创建一个UniTask,当CancellationToken被取消时,这个UniTask将被成功终止。


3.自定义生命周期

当检测到取消时,所有方法都会抛出`OperationCanceledException`并向上游传播。当异常(不限于`OperationCanceledException`)在异步方法中没有被处理时,它最终被传播到`UniTaskScheduler.UnobservedTaskException`。

收到未处理的异常的默认行为是将日志作为异常写入。可以使用`UniTaskScheduler.UnobservedExceptionWriteLogType`来改变日志级别。

如果你想使用自定义行为,请为`UniTaskScheduler.UnobservedTaskException`设置一个action。

还有`OperationCanceledException`是一个特殊的异常,这在`UnobservedTaskException`中会被默默地忽略。

a.如果你想在一个异步UniTask方法中 取消 行为,请手动抛出OperationCanceledException:

public async UniTask<int> FooAsync()
    await UniTask.Yield();
    throw new OperationCanceledException();


b.如果你处理了一个异常,但想 忽略 (传播到global cancellation handling),请使用一个异常过滤器

public async UniTask<int> BarAsync()
        var x = await FooAsync();
        return x * 2;
    catch (Exception ex) when (!(ex is OperationCanceledException)) // when (ex is not OperationCanceledException) at C# 9.0
        return -1;


c.throws/catch `OperationCanceledException` 会稍微繁重,因此 如果你关心性能,请使用 `UniTask.SuppressCancellationThrow` 来避免 OperationCanceledException 抛出 。它会返回 `(bool IsCanceled, T Result)`,而不是抛出。

var (isCanceled, _) = await UniTask.DelayFrame(10, cancellationToken: cts.Token).SuppressCancellationThrow();
if (isCanceled)
    // ...

注意:只有当你直接调用到最源头方法时才会抑制抛出。否则,返回值将被转换,但整个管道将不会抑制抛出


超时处理

1.CancelAfterSlim

超时是取消的一种变体。您可以通过CancellationTokenSouce.CancelAfterSlim(TimeSpan)设置超时,并将 CancellationToken 传递给异步方法

脚本示例:

var cts = new CancellationTokenSource();
cts.CancelAfterSlim(TimeSpan.FromSeconds(5)); // 5秒超时
    await UnityWebRequest.Get("http://foo").SendWebRequest().WithCancellation(cts.Token);
catch (OperationCanceledException ex)
    if (ex.CancellationToken == cts.Token)
        UnityEngine.Debug.Log("Timeout");

注意

CancellationTokenSouce.CancelAfter是一个C#标准api,但是在Unity中你不应该使用它,因为它依赖于线程定时器。而上文中的CancelAfterSlim是UniTask的扩展方法,它使用PlayerLoop代替。


2. CreateLinkedTokenSource (timeout 与其他cancellation(取消来源)一起使用)

var cancelToken = new CancellationTokenSource();
cancelButton.onClick.AddListener(()=>
    cancelToken.Cancel(); //点击按钮取消
var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfterSlim(TimeSpan.FromSeconds(5)); //5秒超时
    //组合token
    var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, timeoutToken.Token);
    await UnityWebRequest.Get("http://foo").SendWebRequest().WithCancellation(linkedTokenSource.Token);
catch (OperationCanceledException ex)
    if (timeoutToken.IsCancellationRequested)
        UnityEngine.Debug.Log("Timeout.");
    else if (cancelToken.IsCancellationRequested)
        UnityEngine.Debug.Log("Cancel clicked.");


3. TimeoutController (优化减少每次调用异步方法超时的CancellationTokenSource的GC分配)

a.脚本示例:

TimeoutController timeoutController = new TimeoutController(); //设置到字段以供重用。
async UniTask FooAsync()
        //您可以将 timeoutController.Timeout(TimeSpan) 传递给 cancelToken。
        await UnityWebRequest.Get("http://foo").SendWebRequest()
            .WithCancellation(timeoutController.Timeout(TimeSpan.FromSeconds(5)));
        timeoutController.Reset(); //成功时调用Reset(停止超时计时器并准备重用)。
    catch (OperationCanceledException ex)
        if (timeoutController.IsTimeout())
            UnityEngine.Debug.Log("timeout");


b.TimeoutController 与其他cancellation(取消来源)一起使用

new TimeoutController(CancellationToken)

脚本示例:

TimeoutController timeoutController;
CancellationTokenSource clickCancelSource;
void Start()
    this.clickCancelSource = new CancellationTokenSource();
    this.timeoutController = new TimeoutController(clickCancelSource);


c.方法

  • *.Timeout
  • *.TimeoutWithoutException


d.注意事项:

UniTask 有 `.Timeout`, `.TimeoutWithoutException` 方法,但是,如果可能,不要使用这些,请通过 `CancellationToken`。因为 `.Timeout` 工作来自Task的外部,所以无法停止超时Task。 `.Timeout` 表示超时时忽略结果。

如果你将传递 `CancellationToken` 至方法,它会从工作内部执行,因此可以停止执行中的工作。


获取进度

1.简述:

  • Unity 的一些AsyncOperations具有 `ToUniTask(IProgress<float> progress = null,...)` 扩展方法
  • 你不应该使用C#标准API `new System.Progress<T>`,因为它每次都会导致GC。请改用` Cysharp.Threading.Tasks.Progress ` 。


2.创建Progress ( progress factory)

方法:

  • Create
  • CreateOnlyValueChanged(进度值已变更时,才会调用)
  • 对调用方实现IProgress(没有 lambda GC)

Create使用示例:

//定义
var progress = Progress.Create<float>(x => Debug.Log(x));
//-------------------------以下为示例-------------------------------------
var request = await UnityWebRequest.Get("http://google.co.jp")
    .SendWebRequest()
    .ToUniTask(progress: progress);

实现IProgress脚本示例:

public class Foo : MonoBehaviour, IProgress<float>
    public void Report(float value)
        UnityEngine.Debug.Log(value);
    public async UniTaskVoid WebRequest()
        var request = await UnityWebRequest.Get("http://google.co.jp")
            .SendWebRequest()
            .ToUniTask(progress: this);

PlayerLoop

1.基础使用:

UniTask 是在一个自定义的 PlayerLoop 上运行的。UniTask 基于PlayerLoop的方法(例如 `Delay`、 `DelayFrame`、 `asyncOperation.ToUniTask` 等...)接受这个 `PlayerLoopTiming`参数。

它指示何时运行,你可以查看 PlayerLoopList.md 用Unity 的默认PlayerLoop注入 UniTask 的自定义循环。

PlayerLoopTiming:

public enum PlayerLoopTiming
    Initialization = 0,
    LastInitialization = 1,
    EarlyUpdate = 2,
    LastEarlyUpdate = 3,
    FixedUpdate = 4,
    LastFixedUpdate = 5,
    PreUpdate = 6,
    LastPreUpdate = 7,
    Update = 8,
    LastUpdate = 9,
    PreLateUpdate = 10,
    LastPreLateUpdate = 11,
    PostLateUpdate = 12,
    LastPostLateUpdate = 13
#if UNITY_2020_2_OR_NEWER
    TimeUpdate = 14,
    LastTimeUpdate = 15,
#endif


2.常用playerLoop说明

a.yield return null和UniTask.Yield

yield return null和UniTask.Yield相似但不同。 yield return null总是返回下一帧,但UniTask.Yield返回下一个调用。也就是说,在PreUpdate上调用UniTask.Yield(PlayerLoopTiming.Update),会返回同一帧。UniTask.NextFrame()保证返回下一帧,你可以期望它的行为与yield return null完全相同。


b.PlayerLoopTiming.Update类似于yield return null

但是它在Update之前被调用(ScriptRunBehaviourUpdate调用Update和uGUI事件(button.onClick等)


c.yield return null的调用时机

ScriptRunDelayedDynamicFrameRate 时调用 yield return null


d.PlayerLoopTiming.FixedUpdate类似于WaitForFixedUpdate


3.PlayerLoop 初始化:

  • 默认情况下,UniTask的PlayerLoop在[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]初始化


  • 在 BeforeSceneLoad 中调用方法的顺序是不确定的 ,因此如果要在其他 BeforeSceneLoad 方法中使用 UniTask,应尝试在此之前对其进行初始化,脚本示例:
// AfterAssembliesLoaded在BeforeSceneLoad之前被调用
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
public static void InitUniTaskLoop()
    var loop = PlayerLoop.GetCurrentPlayerLoop();
    Cysharp.Threading.Tasks.PlayerLoopHelper.Initialize(ref loop);


4.诊断 UniTask 的PlayerLoop:

a.PlayerLoopHelper.IsInjectedUniTaskPlayerLoop()

诊断 UniTask 的PlayerLoop是否就绪


b.PlayerLoopHelper.DumpCurrentPlayerLoop

将所有当前PlayerLoop记录到控制台


脚本示例:

void Start()
    UnityEngine.Debug.Log("UniTaskPlayerLoop ready? " + PlayerLoopHelper.IsInjectedUniTaskPlayerLoop());
    PlayerLoopHelper.DumpCurrentPlayerLoop();


5.删除未使用 PlayerLoopTiming 注入来略微优化循环开销

三个预设InjectPlayerLoopTimings

  • All (默认)
  • Standard(除LastPostLateUpdate外)
  • Minimum(Update | FixedUpdate | LastPostLateUpdate)

可以自定义组合, 如: `InjectPlayerLoopTimings.Update | InjectPlayerLoopTimings.FixedUpdate | InjectPlayerLoopTimings.PreLateUpdate`


脚本示例(初始化时调用PlayerLoopHelper.Initialize(InjectPlayerLoopTimings)

var loop = PlayerLoop.GetCurrentPlayerLoop();
//最少是 Update | FixedUpdate | LastPostLateUpdate
PlayerLoopHelper.Initialize(ref loop, InjectPlayerLoopTimings.Minimum); 

配置Microsoft.CodeAnalysis.BannedApiAnalyzers

你可以像这样为InjectPlayerLoopTimings.Minimum设置BannedSymbols.txt

F:Cysharp.Threading.Tasks.PlayerLoopTiming.Initialization; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastInitialization; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.EarlyUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastEarlyUpdate; Isn't injected this PlayerLoop in this project.d
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastFixedUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.PreUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastPreUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.PreLateUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastPreLateUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.PostLateUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.TimeUpdate; Isn't injected this PlayerLoop in this project.
F:Cysharp.Threading.Tasks.PlayerLoopTiming.LastTimeUpdate; Isn't injected this PlayerLoop in this project.

可以将 `RS0030` 严重性配置为错误


当前注意事项

1.UniTask.Yield(不带CancellationToken)是一个特殊的类型,返回YieldAwaitable并在YieldRunner上运行。它是最轻量级和最快的

2.PlayerLoopTiming.LastPostLateUpdate不等同于coroutine的yield return new WaitForEndOframe()

3.Coroutine的WaitForEndOfFrame似乎在PlayerLoop完成后才运行。一些需要coroutine的结束帧的方法(Texture2D.ReadPixels,ScreenCapture.CaptureScreenshotAsTexture,CommandBuffer等)在被替换成async/await后不能正确工作。在这些情况下,将MonoBehaviour(coroutine runnner)传递给UniTask.WaitForEndOfFrame。例如,await UniTask.WaitForEndOfFrame(this);是 "yield return new WaitForEndOfFrame() "轻量级的无分配的替代方案。

4.在UniTask中,await直接使用原生生命周期,而WithCancellation和ToUniTask使用指定的生命周期。这通常不是一个特别的问题,但是在LoadSceneAsync中,它会导致在await之后的Start和continuation的顺序不同。所以建议不要使用LoadSceneAsync.ToUniTask。

5.在 stacktrace中,你可以检查它在 playerloop 中的运行位置。

6.如果你导入了Unity的Entities包,那就会在BeforeSceneLoad时将自定义PlayerLoop重置为默认值,并注入ECS的循环。当Unity在UniTask的initialize方法之后调用ECS的inject方法,UniTask将不再工作。

为了解决这个问题,你可以在ECS初始化后,重新初始化UniTask的PlayerLoop,脚本示例:

// 获取ECS Loop。
var playerLoop = ScriptBehaviourUpdateOrder.CurrentPlayerLoop;
// 设置UniTask的PlayerLoop
PlayerLoopHelper.Initialize(ref playerLoop);

返回值(*)

1.async void 与 async UniTaskVoid 对比

  • async void是一个标准的C#Task系统,所以它不能在UniTask系统上运行。
  • async UniTaskVoid 是async UniTask的一个轻量级版本,因为它没有可等待的完成,并立即向UniTaskScheduler.UnobservedTaskException报告错误。如果你不需要await(fire and forget),使用UniTaskVoid会更好,不过需要解除警告,你需要调用Forget()。


2.返回值写法:

  • Task->UniTask
  • Task<T>->UniTask<T>
  • void->UniTaskVoid


3.脚本示例

a.Forget()脚本示例:

public async UniTaskVoid FireAndForgetMethod()
    // 干点啥...
    await UniTask.Yield();
public void Caller()
    FireAndForgetMethod().Forget();

b.UniTask也有Forget方法,它与UniTaskVoid类似,具有相同的效果。然而,如果你完全不使用await,UniTaskVoid会更有效率。脚本示例:

public async UniTask DoAsync()
    // do anything...
    await UniTask.Yield();
public void Caller()
    DoAsync().Forget();

c.`UniTaskVoid` 也可以用于 MonoBehaviour 的 `Start` 方法。脚本示例:

class Sample : MonoBehaviour
    async UniTaskVoid Start()
        // async init code.

注册到Event的异步委托(lambda)

不要使用async void。可以使用UniTask.Action或UniTask.UnityAction,它们都通过async UniTaskVoid lambda创建一个委托。

将异步委托转换为 Action/UnityAction并注册到Event的脚本示例:

Action actEvent;
UnityAction unityEvent; // 特别是在uGUI中使用
// 这是不好的: async void
actEvent += async () => { };
unityEvent += async () => { };
// 这是好的: 通过lamada创建Action
actEvent += UniTask.Action(async () => { await UniTask.Yield(); });
unityEvent += UniTask.UnityAction(async () => { await UniTask.Yield(); });

注意:

Action asyncAction = UniTask.Action(
    async () =>
        Debug.Log("UniTask.Action");
        await UniTask.Delay(1000);

等同于

Action asyncAction =
    () =>
        UniTask.Void(
            async () =>
                Debug.Log("UniTask.Void");
                await UniTask.Delay(1000);

等同于

Action asyncAction =
    () =>
        Func<UniTaskVoid> asyncAction = async () =>
            Debug.Log("UniTask.Void");
            await UniTask.Delay(1000);
        asyncAction().Forget();


补充:

UniTask.Void 直接执行异步委托,无返回值 (即发即弃,Foget会被自动调用)

UniTask.Void(async()=>{Debug.Log("UniTask.Void");awaitUniTask.Delay(1000);});

委托中生成UniTask的三种工厂方法

1.脚本定义:

public static UniTask<T> Create<T>(Func<UniTask<T>>factory)
public static UniTask<T> Defer<T>(Func<UniTask<T>>factory)
public static AsyncLazy<T> Lazy<T>(Func<UniTask<T>>factory)


2.他们仨参数都相同,但它们的行为略有不同

方法名 创建时机 备注(执行时机,注意事项)
UniTask.Create 在该方法被调用的时候,立即创建一个新的UniTask UniTask在Create()被调用的那一刻就开始执行了
UniTask.Defer 将UniTask创建延迟到await的时机 只能await一次,但比Lazy轻
UniTask.Lazy 将UniTask创建延迟到await的时机 生成AsyncLazy,AsyncLazy.Task可以被await任意次数;它比Defer的成本更高


3.脚本演示:

a.UniTask.Create 如何快速创建UniTask,并且立即执行

UniTask.Create(
  async ()=> 
    Debug.Log("Create");
    await UniTask.Delay(1000);
    return "11"; 


b.UniTask.Defer 快速创建UniTask,创建时不执行,await时才执行

var defer = UniTask.Defer(
    async () => 
       Debug.Log("defer");
       await UniTask.Delay(1000);
       return "defer";
await defer;
//注意这里不能多次await,否则报错



c.UniTask.Lazy 创建AsyncLazy类型的对象,在创建时不执行,在await时执行,与Defer不同,可以await任意次数

var asyncLazy = UniTask.Lazy(
  async () =>
    Debug.Log("asyncLazy");
    await UniTask.Delay(1000);
    return "asyncLazy";
await asyncLazy.Task;
await asyncLazy.Task;

等待JobSystem的JobHandle

WaitAsync已被添加到JobSystem中的JobHandle。

通过使用这个,你可以切换到任何PlayerLoopTime,然后等待完成


UniTaskTracker

1.简述

对检查(泄漏的)UniTask很有用。你可以在Window -> UniTask Tracker中打开追踪器窗口。

2.窗口按钮功能

  • Enable AutoReload(Toggle) - 自动重新加载
  • Reload -重新加载
  • GC.Collect - 调用GC.Collect
  • Enable Tracking(Toggle) - 开始跟踪async/await UniTask。性能影响:低
  • Enable StackTrace(Toggle) - 当Task启动时捕获堆栈跟踪。性能影响:高

3.注意事项

UniTaskTracker仅用于调试,因为启用跟踪和捕获堆栈跟踪很有用,但对性能影响很大。推荐的用法是同时启用跟踪和堆栈跟踪,以发现Task泄漏,并在完成后同时禁用它们。


与原生Task的API对比

使用原生类型:

.NET 类型 UniTask 类型
IProgress<T> ---
CancellationToken ---
CancellationTokenSource ---

使用 UniTask 类型:

.NET 类型 UniTask 类型
Task/ ValueTask UniTask
Task<T>/ ValueTask<T> UniTask<T>
async void async UniTaskVoid
+= async () => { } UniTask.Void, UniTask.Action, UniTask.UnityAction
--- UniTaskCompletionSource
TaskCompletionSource<T> UniTaskCompletionSource<T>/ AutoResetUniTaskCompletionSource<T>
ManualResetValueTaskSourceCore<T> UniTaskCompletionSourceCore<T>
IValueTaskSource IUniTaskSource
IValueTaskSource<T> IUniTaskSource<T>
ValueTask.IsCompleted UniTask.Status.IsCompleted()
ValueTask<T>.IsCompleted UniTask<T>.Status.IsCompleted()
new Progress<T> Progress.Create<T>
CancellationToken.Register(UnsafeRegister) CancellationToken.RegisterWithoutCaptureExecutionContext
CancellationTokenSource.CancelAfter CancellationTokenSource.CancelAfterSlim
Channel.CreateUnbounded<T>(false){ SingleReader = true} Channel.CreateSingleConsumerUnbounded<T>
IAsyncEnumerable<T> IUniTaskAsyncEnumerable<T>
IAsyncEnumerator<T> IUniTaskAsyncEnumerator<T>
IAsyncDisposable IUniTaskAsyncDisposable
Task.Delay UniTask.Delay
Task.Yield UniTask.Yield
Task.Run UniTask.RunOnThreadPool
Task.WhenAll UniTask.WhenAll
Task.WhenAny UniTask.WhenAny
Task.CompletedTask UniTask.CompletedTask
Task.FromException UniTask.FromException
Task.FromResult UniTask.FromResult
Task.FromCanceled UniTask.FromCanceled
Task.ContinueWith UniTask.ContinueWith
TaskScheduler.UnobservedTaskException UniTaskScheduler.UnobservedTaskException

UniTask其他注意事项

1.线程池限制

大多数 UniTask 方法在单个线程(PlayerLoop)上运行,只有 `UniTask.Run`( `Task.Run` 等效的)和 `UniTask.SwitchToThreadPool` 在线程池上运行。如果你使用线程池,它将无法与 WebGL 等一起工作。

`UniTask.Run` 现在已被取代。你可以改用 `UniTask.RunOnThreadPool`。并且还要考虑是否可以使用 `UniTask.Create` 或 `UniTask.Void`


2.promise对象池配置

UniTask积极地缓存异步promise对象,以实现零GC(关于技术细节,见博文UniTask v2 - Zero Allocation async/await for Unity, with Asynchronous LINQ )。默认情况下,它缓存了所有的promise

方法:

TaskPool.SetMaxPoolSize 每种类型的缓存大小

TaskPool.GetCacheSizeInfo 返回当前池中的缓存对象


3.IEnumerator.ToUniTask 限制

不支持WaitForEndOframe/WaitForFixedUpdate/Coroutine

生命周期与StartCoroutine不一样,它使用指定的PlayerLoopTiming,默认的PlayerLoopTiming.Update会在MonoBehaviour的Update和StartCoroutine的循环之前运行。

如果你想完全兼容从coroutine到async的转换,使用IEnumerator.ToUniTask(MonoBehaviour coroutineRunner)重载。它在参数MonoBehaviour的一个实例上执行StartCoroutine,并在UniTask中等待它的完成。


4.对于UnityEditor

  • UniTask 可以像编辑器协同程序一样在 Unity 编辑器上运行。但是,也有一些限制,
    UniTask.Delay 的 DelayType.DeltaTime、UnscaledDeltaTime 无法正常工作,因为它们无法在编辑器中获取 deltaTime。
  • 因此在 EditMode 上运行时,会自动将 DelayType 改为 `DelayType.Realtime`,await合适的时间。
  • 所有的PlayerLoopTiming都在EditorApplication.update的定时下运行。
  • `-batchmode` 和`-quit` 不起作用,因为 Unity 不会运行 `EditorApplication.update` 并在一帧后退出。
  • 请不要使用 `-quit`,用`EditorApplication.Exit(0)`手动退出 。


5.Profiler下的GC

在UnityEditor中,Profiler显示了编译器生成的AsyncStateMachine的分配,但它只发生在调试(开发)构建中。C#编译器在 "Debug 构建 "时将AsyncStateMachine生成为类,在 "Release构建 "时生成为结构。

Unity从2020.1开始支持代码优化选项(右下脚)。

你可以将C#编译器优化改为release,以在开发构建中移除AsyncStateMachine的GC。这个优化选项也可以通过Compilation.CompilationPipeline-codeOptimization,和Compilation.CodeOptimization来设置。


6.UniTaskSynchronizationContext

Unity默认的SynchronizationContext(UnitySynchronizationContext)在性能上是一个很差的实现。UniTask绕过了SynchronizationContext(和ExecutionContext),所以它不使用它,但如果存在于asyn Task中,仍然使用它。UniTaskSynchronizationContext是UnitySynchronizationContext的替代品,其性能更好。

案例:

public class SyncContextInjecter
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]