1. 项目概述为什么Unity异步任务需要“取消”在Unity开发中尤其是处理网络请求、资源加载、复杂AI行为树或者长时间运行的协程时我们经常会遇到一个棘手的问题如何优雅地中断一个正在执行的任务想象一下玩家在加载场景时点击了“取消”按钮或者一个角色在寻路中途目标点发生了变化如果后台的任务还在傻乎乎地继续运行不仅浪费CPU和内存资源更可能导致各种诡异的Bug比如对象引用丢失、状态不一致甚至引发崩溃。传统的做法是什么可能是设置一个全局的bool isCancelled标志位在Update里不断检查或者在协程里用yield break强行退出。这些方法不仅笨拙、侵入性强而且难以管理特别是当任务嵌套、依赖关系复杂时代码会迅速变成一团乱麻。CancellationToken正是为了解决这个问题而生的“终极武器”。它源自 .NET 的System.Threading命名空间是C#异步编程模型TAP的核心组件之一。Unity 2021 LTS及以上版本对 .NET Standard 2.1 和 C# 8.0 有了更好的支持使得我们可以将这套成熟、强大的任务取消机制无缝引入到Unity项目中。它不仅仅是一个“取消开关”更是一套完整的、可组合的、可传递的“权限控制”系统。一个CancellationToken可以被传递给多个任务实现“一处取消处处响应”多个CancellationToken还可以通过CancellationTokenSource.CreateLinkedTokenSource链接起来形成复杂的取消逻辑。掌握它意味着你能写出响应更迅速、资源管理更精细、代码更健壮的游戏逻辑。这不仅是技术上的优化更是对玩家体验的负责——没有人愿意看着一个无法取消的加载条干着急。2. 核心概念深度解析CancellationTokenSource与CancellationToken要理解这套机制首先要分清两个核心类CancellationTokenSource(CTS) 和CancellationToken(CT)。2.1 CancellationTokenSource取消指令的发射器你可以把CancellationTokenSource想象成一个遥控器的发射端。它是产生取消信号、管理取消状态的源头。主要职责包括生成Token通过其Token属性产生一个或多个关联的CancellationToken。发起取消调用Cancel()或CancelAfter(milliseconds)方法向所有由其产生的Token发出取消指令。资源释放任务完成后应调用Dispose()方法释放资源尤其是在频繁创建CTS的场景中如每次请求创建一个。一个常见的误区是直接使用CancellationToken来取消任务。实际上CT本身是只读的、不可变的它只是一个“信号接收器”。所有的取消操作都必须通过其对应的CancellationTokenSource来执行。// 创建一个取消源 CancellationTokenSource cts new CancellationTokenSource(); // 从源获取一个令牌信号接收器 CancellationToken token cts.Token; // ... 将 token 传递给异步任务 ... // 在某个条件满足时如玩家点击取消触发取消 if (playerRequestedCancel) { cts.Cancel(); // 发射取消信号 } // 任务结束后记得清理 cts.Dispose();2.2 CancellationToken取消信号的接收与检查器CancellationToken则是遥控器的接收端它被传递给需要支持取消的异步方法或任务。它的核心作用是让任务能够“感知”外部的取消请求。它提供了几种方式来响应取消轮询检查在任务代码中定期检查token.IsCancellationRequested属性。如果为true则应立即停止工作进行清理并退出。async Task LoadSceneAsync(CancellationToken token) { for (int i 0; i 100; i) { // 每次循环都检查是否被取消 if (token.IsCancellationRequested) { Debug.Log(任务被取消正在清理...); // 执行清理逻辑如回滚已加载的部分资源 return; } // 模拟加载工作 await Task.Delay(50); UpdateProgressBar(i); } }抛出异常调用token.ThrowIfCancellationRequested()。这是一个更简洁、更标准化的方式。如果取消已被请求该方法会立即抛出OperationCanceledException。这通常用于在异步方法内部快速失败并将取消状态向上层传递。async Taskint ComplexCalculationAsync(CancellationToken token) { await Task.Delay(1000, token); // Delay 本身也支持 CancellationToken token.ThrowIfCancellationRequested(); // 如果在上一步Delay期间被取消这里会抛出异常 // ... 其他计算 ... return result; }注册回调通过token.Register(Action callback)方法注册一个在取消发生时被调用的回调函数。这对于释放非托管资源或更新UI状态非常有用。CancellationToken token cts.Token; // 注册一个取消时的回调 token.Register(() { Debug.Log(任务取消关闭网络连接或清理临时文件。); progressBar.gameObject.SetActive(false); });注意ThrowIfCancellationRequested抛出的OperationCanceledException是一种特殊的异常它通常不应该被当作错误来处理而应被视为一种正常的控制流。在调用异步方法时通常使用try-catch来捕获它并据此决定是继续执行其他逻辑还是通知用户操作已取消。2.3 链接令牌实现复杂的取消策略这是CancellationToken系统最强大的功能之一。假设你有两个取消源一个来自用户界面如取消按钮另一个来自超时计时器。你希望任务在任意一个条件触发时都能取消。这时就可以使用链接令牌。// 创建两个独立的取消源 CancellationTokenSource uiCts new CancellationTokenSource(); // UI取消 CancellationTokenSource timeoutCts new CancellationTokenSource(); // 超时取消 timeoutCts.CancelAfter(5000); // 5秒后自动取消 // 创建一个链接令牌源将两个令牌链接起来 using (var linkedCts CancellationTokenSource.CreateLinkedTokenSource(uiCts.Token, timeoutCts.Token)) { CancellationToken linkedToken linkedCts.Token; try { await LongRunningTaskAsync(linkedToken); Debug.Log(任务完成); } catch (OperationCanceledException) { // 判断是哪个源触发的取消虽然不总是必要 if (timeoutCts.IsCancellationRequested) Debug.LogWarning(任务超时); else if (uiCts.IsCancellationRequested) Debug.Log(用户取消了操作。); } } // using语句会自动Dispose linkedCts通过链接你可以构建出非常灵活的取消策略例如“全局游戏退出信号 单个任务超时信号”的组合这为大型项目的资源管理和生命周期控制提供了坚实的基础。3. 在Unity中的实战应用场景与模式理解了基本原理后我们来看看如何在Unity的各种常见场景中应用CancellationToken。3.1 场景一可取消的异步资源加载这是最经典的应用。Unity的Addressable Assets系统或UnityWebRequest已经原生支持CancellationToken。using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; using System.Threading; using System.Threading.Tasks; public class CancellableAssetLoader : MonoBehaviour { private CancellationTokenSource _loadingCts; public async TaskGameObject LoadPrefabAsync(string assetKey, CancellationToken externalToken default) { // 创建一个新的CTS专门用于这次加载 _loadingCts CancellationTokenSource.CreateLinkedTokenSource(externalToken); CancellationToken linkedToken _loadingCts.Token; try { // Addressables的LoadAssetAsync有一个重载接受CancellationToken AsyncOperationHandleGameObject handle Addressables.LoadAssetAsyncGameObject(assetKey); // 手动将取消令牌与AsyncOperationHandle关联 // 注意Addressables API可能不会自动中断下载但我们可以通过注册回调和检查来模拟 linkedToken.Register(() { if (handle.IsValid() !handle.IsDone) { Debug.Log($取消加载: {assetKey}); Addressables.Release(handle); // 关键释放未完成的Handle防止内存泄漏 } }); // 等待加载完成同时定期检查取消 while (!handle.IsDone) { linkedToken.ThrowIfCancellationRequested(); await Task.Yield(); // 让出一帧避免阻塞主线程 } linkedToken.ThrowIfCancellationRequested(); // 最终检查 return handle.Result; } catch (OperationCanceledException) { Debug.Log($资产 {assetKey} 的加载已被取消。); return null; } finally { _loadingCts?.Dispose(); _loadingCts null; } } // 供UI按钮调用的取消方法 public void CancelCurrentLoading() { _loadingCts?.Cancel(); } }实操心得内存泄漏陷阱取消异步资源加载时最关键的一步是释放Release对应的AsyncOperationHandle。如果只取消而不释放加载操作可能仍在后台进行或资源引用被持有导致内存泄漏。Addressables.Release(handle)是必须的。链接外部Token将方法参数externalToken与本地CTS链接是个好习惯。这样调用方如一个场景加载管理器可以传递一个全局的取消令牌如“退出游戏”令牌实现层级式的取消控制。3.2 场景二改造传统协程支持取消Unity的Coroutine本身不支持CancellationToken但我们可以通过一些模式来桥接。模式A在协程内部轮询MonoBehaviour的标记public class CancellableCoroutineRunner : MonoBehaviour { private bool _isCancelled false; public void StartCancellableTask() { _isCancelled false; StartCoroutine(MyLongCoroutine()); } public void CancelTask() { _isCancelled true; } private IEnumerator MyLongCoroutine() { for (int i 0; i 100; i) { if (_isCancelled) { Debug.Log(协程被取消); yield break; // 使用 yield break 退出协程 } // ... 一些工作 ... yield return new WaitForSeconds(0.1f); } } }这种方式简单但无法与基于CancellationToken的异步生态系统集成。模式B使用CancellationToken驱动协程更推荐我们可以创建一个适配器让协程的每一步都检查CancellationToken。public static class CoroutineExtensions { // 一个辅助方法将基于Token的异步等待融入协程 public static IEnumerator WithCancellation(this IEnumerator enumerator, CancellationToken token) { while (true) { // 在每次移动之前检查是否取消 if (token.IsCancellationRequested) { Debug.Log(协程因CancellationToken而取消); yield break; } if (!enumerator.MoveNext()) { yield break; } // 返回当前值可能是 WaitForSeconds, WaitForEndOfFrame 等 yield return enumerator.Current; } } } // 使用方式 public class ImprovedRunner : MonoBehaviour { private CancellationTokenSource _cts; void Start() { _cts new CancellationTokenSource(); StartCoroutine(MyBetterCoroutine().WithCancellation(_cts.Token)); } private IEnumerator MyBetterCoroutine() { // 现在这个协程体不需要关心取消逻辑 for (int i 0; i 100; i) { Debug.Log($Working... {i}); yield return new WaitForSeconds(0.1f); } } void OnDestroy() { _cts?.Cancel(); _cts?.Dispose(); } }这种方式将取消关注点分离协程逻辑更清晰并且能与外部统一的CancellationToken系统联动。3.3 场景三网络请求的超时与取消对于UnityWebRequest或HttpClient结合CancellationToken可以实现强大的超时和用户取消控制。using UnityEngine; using UnityEngine.Networking; using System.Threading; using System.Threading.Tasks; public class NetworkService { private CancellationTokenSource _requestCts; private const int DefaultTimeoutMs 10000; public async Taskstring GetJsonWithTimeoutAsync(string url, int timeoutMs DefaultTimeoutMs, CancellationToken externalToken default) { // 创建链接令牌外部令牌 超时令牌 _requestCts CancellationTokenSource.CreateLinkedTokenSource(externalToken); _requestCts.CancelAfter(timeoutMs); using (UnityWebRequest request UnityWebRequest.Get(url)) { var operation request.SendWebRequest(); // 注册取消回调取消时中止网络请求 _requestCts.Token.Register(() { if (!request.isDone) { Debug.LogWarning($请求被取消: {url}); request.Abort(); // 关键中止底层网络操作 } }); try { // 等待完成同时响应取消 while (!operation.isDone) { _requestCts.Token.ThrowIfCancellationRequested(); await Task.Yield(); } _requestCts.Token.ThrowIfCancellationRequested(); // 最终检查 if (request.result UnityWebRequest.Result.Success) { return request.downloadHandler.text; } else { // 如果是因Abort导致的错误可以特殊处理 if (request.result UnityWebRequest.Result.ConnectionError) { throw new TaskCanceledException(网络请求被取消或超时。); } throw new Exception($网络错误: {request.error}); } } catch (OperationCanceledException) { // 统一处理取消异常 Debug.Log($请求 {url} 被取消。); throw; // 可以选择重新抛出让上层处理 } finally { request.Dispose(); _requestCts?.Dispose(); _requestCts null; } } } }关键点UnityWebRequest.Abort()是立即终止请求的核心方法。仅仅设置CancellationToken而不调用Abort()请求可能在后台继续消耗带宽和资源。4. 高级模式与架构设计当项目规模扩大你需要一套系统性的取消策略。4.1 全局取消令牌管理游戏生命周期创建一个全局的、代表游戏或场景生命周期的CancellationTokenSource是非常有用的模式。public class GameLifecycleManager : MonoBehaviour { public static GameLifecycleManager Instance { get; private set; } // 全局令牌源当游戏退出或场景卸载时取消 private CancellationTokenSource _globalCts; public CancellationToken GlobalToken _globalCts?.Token ?? default; void Awake() { if (Instance ! null Instance ! this) { Destroy(this.gameObject); return; } Instance this; DontDestroyOnLoad(this.gameObject); _globalCts new CancellationTokenSource(); } void OnDestroy() { // 当管理器销毁时如游戏退出触发全局取消 _globalCts?.Cancel(); _globalCts?.Dispose(); _globalCts null; } // 当加载新场景时可以重置或创建一个新的全局令牌 public void OnSceneWillUnload() { _globalCts?.Cancel(); // 取消所有与旧场景相关的任务 _globalCts?.Dispose(); _globalCts new CancellationTokenSource(); // 为新场景创建新令牌 } } // 在其他任何需要长时间运行的任务中 public class SomeService : MonoBehaviour { async void Start() { // 将全局令牌传递给任务确保游戏退出时任务能自动停止 await SomeLongTaskAsync(GameLifecycleManager.Instance.GlobalToken); } }这样所有后台任务都关联了全局令牌当玩家退出游戏或切换场景时一键即可安全清理所有后台活动避免“后台幽灵任务”引发的问题。4.2 分层取消UI、业务、资源的权限隔离在复杂UI流程中你可能需要更细粒度的控制UI层令牌一个弹出窗口有自己的CancellationTokenSource窗口关闭时取消所有内部动画和请求。业务逻辑层令牌一个特定的功能模块如战斗系统有自己的令牌当模块禁用时取消所有相关计算。资源层令牌与全局令牌链接确保资源加载在游戏退出时能被终止。你可以通过创建链接令牌来建立这种层级关系// 在UI窗口中 public class PopupWindow : MonoBehaviour { private CancellationTokenSource _windowCts; void OnEnable() { _windowCts new CancellationTokenSource(); } void OnDisable() { _windowCts?.Cancel(); _windowCts?.Dispose(); } public CancellationToken WindowToken _windowCts.Token; public void StartAsyncOperation() { // 将窗口令牌与全局令牌链接这样无论窗口关闭还是游戏退出操作都会停止 var linkedTokenSource CancellationTokenSource.CreateLinkedTokenSource( GameLifecycleManager.Instance.GlobalToken, WindowToken ); _ DoOperationAsync(linkedTokenSource.Token); } }4.3 与UniTask等第三方库的结合如果你在项目中使用像UniTask这样强大的Unity异步扩展库那么对CancellationToken的支持会更加丝滑。UniTask 几乎为所有 Unity 异步操作如等待帧、等待物理更新、等待资源加载都提供了原生的CancellationToken支持并且性能开销更低。using Cysharp.Threading.Tasks; using UnityEngine; public class UniTaskCancellationExample : MonoBehaviour { private CancellationTokenSource _cts; async UniTaskVoid Start() { _cts new CancellationTokenSource(); try { // UniTask 的 Delay 和大部分异步操作都直接支持 CancellationToken await UniTask.Delay(3000, cancellationToken: _cts.Token); // 加载资源也支持 var prefab await Resources.LoadAsyncGameObject(Prefab).WithCancellation(_cts.Token); Debug.Log(操作完成); } catch (OperationCanceledException) { Debug.Log(任务被取消。); } } void OnDestroy() { _cts?.Cancel(); _cts?.Dispose(); } }使用 UniTask你可以用更简洁的语法实现同样的取消逻辑并且它解决了Unity主线程上下文切换的一些痛点是大型项目的强力推荐。5. 常见陷阱、调试技巧与最佳实践即使理解了概念在实际编码中依然会踩坑。下面是一些血泪教训总结。5.1 陷阱一CancellationTokenSource未释放Dispose这是一个常见的资源泄漏点。CancellationTokenSource内部可能持有定时器或其他资源。如果不调用Dispose在频繁创建和取消的场景下如每帧会导致内存和性能问题。最佳实践对于MonoBehaviour中生命周期明确的CTS在OnDestroy中Cancel并Dispose。对于局部使用的CTS使用using语句块确保释放。using (var cts new CancellationTokenSource()) { await SomeTaskAsync(cts.Token); } // 这里会自动调用 cts.Dispose()对于链接的CancellationTokenSource同样需要Dispose。using语句会处理它创建的对象但传入链接的源需要你自己管理。5.2 陷阱二在取消后访问已释放的资源当取消触发时任务可能在中途停止。如果任务正在操作某个Unity对象如GameObject、Texture并且在取消回调或catch块中尝试访问它而这个对象可能已经被销毁例如因为场景切换就会抛出MissingReferenceException。防御性编程private async Task LoadAndInstantiateAsync(CancellationToken token) { GameObject loadedPrefab null; try { loadedPrefab await LoadPrefabFromAddressables(key, token); token.ThrowIfCancellationRequested(); // 实例化前再次检查对象是否有效针对Unity对象 if (loadedPrefab null) return; Instantiate(loadedPrefab, transform.position, Quaternion.identity); } catch (OperationCanceledException) { Debug.Log(加载取消); // 如果取消发生在加载之后、实例化之前loadedPrefab可能非空但也不需要处理了 // Addressables系统会在Handle释放时管理其生命周期 } catch (Exception e) { Debug.LogError($加载失败: {e}); } }对于Unity对象在取消后的清理代码中使用GameObject.ReferenceEquals(obj, null)或(obj null)进行安全检查是必要的但要注意Unity重载了运算符对于被销毁的对象会返回true。5.3 陷阱三忽略异步方法内部的异常传播有些异步操作尤其是那些包装了原生Unity协程的操作在取消时可能不会抛出OperationCanceledException而是通过返回null或默认值或者设置一个状态标志。你需要仔细阅读所用API的文档。应对策略查阅文档确认你使用的异步API如Addressables, UnityWebRequest, UniTask在取消时的具体行为。双重检查在catch (OperationCanceledException)块之后检查任务返回的结果是否有效。统一包装如果使用的库不支持可以自己编写一个包装方法在检测到取消时主动抛出异常。5.4 调试技巧可视化与日志在开发阶段为CTS添加调试信息非常有用。public class DebuggableCancellationTokenSource : CancellationTokenSource { public string SourceName { get; } public StackTrace CreationStackTrace { get; } public DebuggableCancellationTokenSource(string name) { SourceName name; CreationStackTrace new StackTrace(2, true); // 跳过本构造函数和调用者的栈帧 } public new void Cancel() { Debug.Log($【Cancellation】触发取消源: {SourceName}); Debug.Log(CreationStackTrace.ToString()); base.Cancel(); } }使用这个可调试的源当取消发生时你就能在控制台看到是哪个源、在何处创建的大大简化了复杂系统中取消信号的溯源。5.5 最佳实践清单始终传递Token设计任何可能长时间运行的异步方法时都添加一个CancellationToken cancellationToken default参数。即使当前用不到也为未来留出扩展空间。及时释放资源使用using语句或在生命周期结束时调用Dispose()。使用链接令牌将局部取消信号与全局生命周期令牌链接起来构建安全的取消链。清理Unity对象引用在取消回调中谨慎处理可能已被销毁的Unity对象。区分取消与错误OperationCanceledException不是错误是正常控制流。避免用它来掩盖真正的程序错误。测试取消路径像测试正常流程一样专门测试任务被取消时的行为资源是否释放、状态是否回滚、UI是否更新。文档化取消行为在代码注释或API文档中说明你的方法在接收到取消信号时的具体行为是立即返回null还是抛出异常是否会进行部分清理。将CancellationToken融入你的Unity开发工作流初期可能需要一些思维转变但一旦习惯你会发现代码的健壮性和可维护性会得到质的提升。它就像为你的异步任务装上了精准的刹车和方向盘让你能完全掌控它们的生命周期。