UnitTask中的Forget()与 CTS

🎯 结论先行:不是“每个异步方法”都要加 CTS

而是「凡是可能长时间存在、或受生命周期控制的任务」才应该加 CTS。


🧩 一、Forget()CTS 的核心区别

对比项 .Forget() CancellationTokenSource
目的 忽略等待结果 控制任务生命周期
适用场景 轻量、短期任务 长时、可取消任务
是否能终止任务 ❌ 否 ✅ 可以取消
是否捕获异常 ❌ 默认丢弃(除非 .Forget(ResultHandling...)) ✅ 可被 try-catch 捕获
GC 开销 少量(每次创建)

🧭 二、两者协同工作的黄金范式

最佳做法是:

👉「任务逻辑用 CTS 控制生命周期」,
👉「调用者决定是否 await.Forget()」。

示例:

private CancellationTokenSource cts;

private void Start()
{
    cts = new();
    LoadSceneAsync(cts.Token).Forget(); // 调用方选择忽略
}

private async UniTask LoadSceneAsync(CancellationToken token)
{
    await UniTask.Delay(5000, cancellationToken: token);
    Debug.Log("场景加载完成");
}

private void OnDestroy()
{
    cts.Cancel(); // 防止场景切换或对象销毁时任务泄漏
}

💡 解释:

  • LoadSceneAsync 不关心是否被 await;

  • cts.Token 决定任务是否继续;

  • Forget() 让调用方不阻塞,但内部仍受控。


⚙️ 三、什么时候“必须”加 CTS?

场景 是否建议加 CTS 原因
⏳ 延迟逻辑(如 Delay, WaitUntil) ✅ 是 避免对象销毁后继续等待
🔄 循环任务(如 AI 行为、监控) ✅ 是 必须能中断
🌐 异步加载(网络/资源) ✅ 是 用户可能切场景、关闭窗口
⚡ 短期逻辑(如 UI 动画) ❌ 可省略 生命周期很短
🧩 同步逻辑(例如计算) ❌ 不必要 一次执行完即结束
🧠 Editor 模式(非运行时) ✅ 推荐 编辑器中生命周期不稳定

🧱 四、实际对比:有无 CTS 的差异

❌ 无 CTS(任务泄漏)

private async void Start()
{
    await UniTask.Delay(5000);
    Debug.Log("对象早就销毁了,但我还在执行!");
}

✅ 有 CTS(安全终止)

private async void Start()
{
    var token = this.GetCancellationTokenOnDestroy();
    await UniTask.Delay(5000, cancellationToken: token);
    Debug.Log("对象没销毁才会执行到这里");
}

💡 说明:

GetCancellationTokenOnDestroy() 是 UniTask 为 MonoBehaviour 提供的神器,它会在对象销毁时自动取消。


🧩 五、关于 Forget() 的使用建议

✅ 推荐写法:

SomeAsync().Forget(); // 我知道它会跑完,不需要返回结果

⚠️ 不推荐写法:

await SomeAsync().Forget(); // 无意义,Forget返回void

🚨 异常丢失问题:

.Forget() 默认吞异常,你可以这样防御:

SomeAsync().Forget(e => Debug.LogException(e));

🧠 六、实战建议(通俗记忆法)

任务类型 是否加 CTS 是否 Forget
UI 动画
网络请求 ❌(要 await 结果)
AI 行为循环
Editor 扩展操作
一次性逻辑
场景加载 ❌(等待完成)

🔍 七、进阶:组合式 CTS 管理

你可以用一个父级 Token 控制多个异步任务:

var cts = new CancellationTokenSource();

DoA(cts.Token).Forget();
DoB(cts.Token).Forget();
DoC(cts.Token).Forget();

// 一键取消所有
cts.Cancel();

或者组合多个 Token:

var linked = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);
await UniTask.Delay(1000, cancellationToken: linked.Token);

📈 八、性能注意

  • 创建一个 CTS 的开销 ≈ 40~60B GC Alloc;

  • 所以:
    👉 建议在 MonoBehaviour 级别缓存
    👉 而不是每次 async 方法都 new 一个。

private CancellationTokenSource cts;
private void Awake() => cts = new();
private void OnDestroy() => cts.Cancel();

🧩 九、最佳实践模板(生产级推荐)

public class Example : MonoBehaviour
{
    private CancellationTokenSource _cts;

    private void Awake()
    {
        _cts = new();
    }

    private void OnDestroy()
    {
        _cts.Cancel();
        _cts.Dispose();
    }

    private void Start()
    {
        RunAsync(_cts.Token).Forget();
    }

    private async UniTask RunAsync(CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            await UniTask.Delay(1000, cancellationToken: token);
            Debug.Log("持续工作中...");
        }
    }
}

🧠 记忆口诀

Forget 是“不管结局”,CTS 是“能中断结局”。

  • 只要任务可能「超时、循环、依赖对象存在」,就加 CTS

  • 调用方只关心“我是否等待结果”,就用 Forget

posted @ 2025-10-11 21:18  世纪末の魔术师  阅读(41)  评论(1)    收藏  举报