🎯 结论先行:不是“每个异步方法”都要加 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