当前位置: 首页 > news >正文

UniTask CancellationTokenSource实战:优雅处理异步任务取消

1. 为什么需要优雅取消异步任务?

在Unity游戏开发中,异步操作无处不在。比如加载资源、网络请求、角色动画过渡等场景都需要用到异步编程。但实际开发中经常遇到这样的问题:玩家在加载场景时突然切换菜单,或者中断了某个耗时操作,这时候如果后台还在继续执行之前的异步任务,不仅浪费性能,还可能导致各种奇怪的bug。

我遇到过最典型的一个案例是角色技能系统。当玩家快速连续按下不同技能键时,如果前一个技能动画的异步播放没有被正确取消,角色就会出现动作错乱。这时候就需要用到CancellationTokenSource来优雅地终止之前的异步任务。

CancellationTokenSource是C#中用于取消异步操作的核心类,而UniTask作为Unity中的高性能异步方案,完美整合了这个机制。相比传统的协程(Coroutine),UniTask+CancellationToken的组合提供了更强大的控制能力,特别是在任务取消方面。

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的基本工作流程:

  1. 创建CancellationTokenSource实例
  2. 通过Token属性获取关联的CancellationToken
  3. 将token传递给需要支持取消的异步方法
  4. 调用Cancel()方法触发取消操作

2.2 关键点解析

在实际使用中有几个关键点需要注意:

  1. Token传递:CancellationToken应该作为参数显式传递给所有需要支持取消的子方法,形成"取消传播链"。我见过不少开发者只在顶层方法接收token,内部方法却不传递,这会导致取消信号无法正确传递到所有相关操作。

  2. 资源释放:CancellationTokenSource实现了IDisposable接口,使用完后应该调用Dispose()释放资源。最佳实践是在MonoBehaviour的OnDestroy中处理,就像示例中展示的那样。

  3. 状态检查:在耗时循环中,应该定期检查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($"进度: {i+1}/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 如何选择?

根据我的项目经验,给出以下建议:

  1. 简单场景:如果取消是罕见情况,且需要集中处理错误,用try-catch更直观。

  2. 性能敏感场景:比如每帧执行的异步逻辑,或者高频取消的情况,用SuppressCancellationThrow性能更好。

  3. 复杂调用链:当有深层嵌套的异步调用时,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.GetOrAddComponent<OnDestroyTrigger>() .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");

这样当取消发生时,日志会明确显示是哪个操作被取消了,大大简化调试过程。

http://www.jsqmd.com/news/515822/

相关文章:

  • Qwen3-ASR-1.7B部署避坑指南:RTX3060/4090适配要点与常见报错修复
  • ESP32四路继电器模块SI-1104硬件设计与Arduino控制指南
  • AI编程省钱技巧:手把手教你用Roo Code+Claude 3搭建私有代码补全系统
  • 迅为RK3576多屏显示终极优化:主副屏触摸隔离+鼠标跨屏的底层实现解析
  • Qwen3-32B-Chat企业降本增效实践:替代商用API,私有部署年省数万元成本分析
  • 新手避坑指南:从F450到X450,我的无人机机架升级与分电板焊接实战
  • WPF+Prism实战:5分钟搞定MaterialDesign风格抽屉菜单(附完整源码)
  • OpenClaw+QwQ-32B内容创作流:从大纲生成到多平台发布
  • RobustDcf:工业级DCF77抗干扰解码器设计与实现
  • 几何约束改进RANSAC与卡尔曼滤波(Kalman Filter)的结合
  • 从WAV到蜂鸣器:手把手教你用STM32F103 DAC播放自定义音频片段(基于HAL库)
  • Linux ALSA声卡驱动开发实战:手把手教你配置Cpu_dai参数(附MTK平台示例)
  • 专业开发者指南:AnimatedDrawings配置优化与性能调优完全指南
  • Phi-3-mini-4k-instruct应用场景:Ollama部署支撑学生编程作业智能辅导系统
  • 告别print调试!FastAPI+loguru实现彩色日志与智能回溯的5个技巧
  • EasyAnimateV5-7b-zh-InP入门指南:从零开始创建第一个AI视频
  • DeOldify实战:零基础搭建智能上色Web服务,让回忆重焕光彩
  • Qwen3.5-9B开源模型效果展示:Qwen3.5-9B在MMMU基准表现
  • DIYables ESP32 WebServer:嵌入式轻量级Web服务框架解析
  • 如何高效管理个人音乐收藏?网易云音乐下载器的全场景实践指南
  • Cherry Markdown 0.1.1:多维度文档处理解决方案的技术革新
  • SenseVoice-Small ONNX实现多语言语音识别:Java开发实战
  • Pixel Dimension Fissioner实操:对接LangChain构建文本裂变Agent工作流
  • 终极图片整理方案:AntiDupl让你的数字相册告别混乱
  • 用Kali Linux和Metasploit测试安卓旧手机安全:一次完整的渗透测试实验(附APK生成与监听配置)
  • AI教材编写新利器!低查重一键生成教材,高效完成教学资料创作
  • Clawdbot+Qwen3:32B保姆级教程:Clawdbot CLI常用命令详解——onboard/status/logs/upgrade
  • 别再一个个敲命令了!华为交换机端口组(port-group)批量配置实战,5分钟搞定VLAN划分
  • 南北阁Nanbeige 4.1-3B快速体验:ComfyUI可视化工作流集成方案
  • Xinference-v1.17.1数据库优化实践:提升大模型查询效率50%