
1. 为什么需要优雅取消异步任务在Unity游戏开发中异步操作无处不在。比如加载资源、网络请求、角色动画过渡等场景都需要用到异步编程。但实际开发中经常遇到这样的问题玩家在加载场景时突然切换菜单或者中断了某个耗时操作这时候如果后台还在继续执行之前的异步任务不仅浪费性能还可能导致各种奇怪的bug。我遇到过最典型的一个案例是角色技能系统。当玩家快速连续按下不同技能键时如果前一个技能动画的异步播放没有被正确取消角色就会出现动作错乱。这时候就需要用到CancellationTokenSource来优雅地终止之前的异步任务。CancellationTokenSource是C#中用于取消异步操作的核心类而UniTask作为Unity中的高性能异步方案完美整合了这个机制。相比传统的协程CoroutineUniTaskCancellationToken的组合提供了更强大的控制能力特别是在任务取消方面。2. CancellationTokenSource基础用法2.1 创建与基本使用先来看一个最简单的使用示例private CancellationTokenSource _cts; void Start() { _cts new CancellationTokenSource(); // 启动异步任务 DoAsyncWork(_cts.Token).Forget(); } async UniTask DoAsyncWork(CancellationToken token) { while (!token.IsCancellationRequested) { await UniTask.Delay(1000, cancellationToken: token); Debug.Log(Working...); } Debug.Log(任务已取消); } // 当需要取消时 void OnDestroy() { _cts?.Cancel(); _cts?.Dispose(); }这段代码展示了CancellationTokenSource的基本工作流程创建CancellationTokenSource实例通过Token属性获取关联的CancellationToken将token传递给需要支持取消的异步方法调用Cancel()方法触发取消操作2.2 关键点解析在实际使用中有几个关键点需要注意Token传递CancellationToken应该作为参数显式传递给所有需要支持取消的子方法形成取消传播链。我见过不少开发者只在顶层方法接收token内部方法却不传递这会导致取消信号无法正确传递到所有相关操作。资源释放CancellationTokenSource实现了IDisposable接口使用完后应该调用Dispose()释放资源。最佳实践是在MonoBehaviour的OnDestroy中处理就像示例中展示的那样。状态检查在耗时循环中应该定期检查token.IsCancellationRequested属性这是最轻量级的取消检查方式。3. 两种取消处理方式对比UniTask提供了两种主要的取消处理方式各有适用场景。让我们通过一个更完整的例子来对比分析。3.1 try-catch方式public class TaskCancellationExample : MonoBehaviour { [SerializeField] private Button startButton; [SerializeField] private Button cancelButton; private CancellationTokenSource _cts; void Start() { _cts new CancellationTokenSource(); startButton.onClick.AddListener(() StartTask(_cts.Token).Forget()); cancelButton.onClick.AddListener(() _cts.Cancel()); } async UniTask StartTask(CancellationToken token) { try { await LongRunningTask(token); Debug.Log(任务正常完成); } catch (OperationCanceledException) { Debug.Log(任务被取消); } } async UniTask LongRunningTask(CancellationToken token) { for (int i 0; i 10; i) { token.ThrowIfCancellationRequested(); await UniTask.Delay(1000, cancellationToken: token); Debug.Log($进度: {i1}/10); } } void OnDestroy() { _cts?.Dispose(); } }这种方式的特点使用try-catch捕获OperationCanceledException需要显式调用ThrowIfCancellationRequested()抛出异常代码结构清晰取消逻辑集中处理会产生异常开销性能稍差3.2 SuppressCancellationThrow方式async UniTask StartTask(CancellationToken token) { var (isCanceled, _) await LongRunningTask(token).SuppressCancellationThrow(); if (isCanceled) { Debug.Log(任务被取消); } else { Debug.Log(任务正常完成); } }这种方式的特点使用SuppressCancellationThrow()避免异常抛出返回一个元组第一个bool表示是否被取消性能更好没有异常开销需要处理返回值代码稍显分散3.3 如何选择根据我的项目经验给出以下建议简单场景如果取消是罕见情况且需要集中处理错误用try-catch更直观。性能敏感场景比如每帧执行的异步逻辑或者高频取消的情况用SuppressCancellationThrow性能更好。复杂调用链当有深层嵌套的异步调用时SuppressCancellationThrow可以避免多层try-catch带来的代码混乱。4. 实际开发中的进阶技巧4.1 超时自动取消CancellationTokenSource有个很实用的功能是设置超时自动取消// 5秒后自动取消 var cts new CancellationTokenSource(TimeSpan.FromSeconds(5)); // 或者手动设置 cts.CancelAfter(TimeSpan.FromSeconds(5));这个功能在网络请求超时处理中特别有用。我在一个手游项目中就用它来处理弱网环境下的资源加载当加载时间超过设定阈值就自动取消并回退到低清资源。4.2 组合Token有时候我们需要组合多个取消条件比如既要支持手动取消又要有超时限制var timeoutCTS new CancellationTokenSource(TimeSpan.FromSeconds(5)); var manualCTS new CancellationTokenSource(); // 创建组合token var linkedCTS CancellationTokenSource.CreateLinkedTokenSource( timeoutCTS.Token, manualCTS.Token ); // 使用组合token await DoSomethingAsync(linkedCTS.Token);这样无论是超时还是手动调用manualCTS.Cancel()都会触发任务取消。4.3 与Unity生命周期集成Unity开发中一个常见需求是把异步任务和GameObject生命周期绑定。我们可以这样实现public static class UniTaskExtensions { public static CancellationToken GetCancellationTokenOnDestroy(this GameObject gameObject) { var cts new CancellationTokenSource(); // 当对象销毁时取消 gameObject.GetOrAddComponentOnDestroyTrigger() .OnDestroyEvent () cts.Cancel(); return cts.Token; } } // 使用方式 async void Start() { var token this.gameObject.GetCancellationTokenOnDestroy(); await SomeAsyncOperation(token); }这个扩展方法让我们可以方便地创建与GameObject生命周期关联的token当对象销毁时自动取消关联的异步任务。5. 常见问题与解决方案5.1 取消后资源清理异步任务取消后经常需要清理已经创建的资源。推荐使用try-finally模式async UniTask LoadAssetAsync(CancellationToken token) { AssetLoader loader null; try { loader new AssetLoader(); await loader.LoadAsync(path/to/asset, token); // 使用asset... } finally { loader?.Dispose(); } }即使操作被取消finally块中的代码也会执行确保资源正确释放。5.2 取消与UI交互处理UI按钮的异步操作时需要特别注意竞态条件。下面是一个安全的实现模式private CancellationTokenSource _uiOperationCTS; public async void OnButtonClick() { // 取消之前的操作 _uiOperationCTS?.Cancel(); // 创建新的CTS _uiOperationCTS new CancellationTokenSource(); try { await DoUIAsyncOperation(_uiOperationCTS.Token); } catch (OperationCanceledException) { // 忽略取消异常 } }这样可以确保每次点击按钮都会取消前一个未完成的操作避免多个异步操作同时进行导致的状态混乱。5.3 调试技巧调试异步取消逻辑时可以给CancellationTokenSource设置名字以便区分var cts new CancellationTokenSource(); cts.Token.Register(() Debug.Log(NetworkRequest canceled)); // 或者在高级场景中 var cts new CancellationTokenSource(); Debugger.AttachToCancellationToken(cts.Token, SceneLoading);这样当取消发生时日志会明确显示是哪个操作被取消了大大简化调试过程。