9.取消操作
9.1 知识点
原生 Task 取消机制详解
测试脚本
using System;
using System.Threading;
using System.Threading.Tasks;
namespace UniTask_Console
{
internal class Program
{
static async Task Main(string[] args)
{
// 创建取消令牌源,用于控制异步操作的取消逻辑
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
// 从令牌源获取取消令牌,用于在异步操作中检测取消请求
CancellationToken cancellationToken = cancellationTokenSource.Token;
// 启动一个异步任务,使用匿名方法定义任务逻辑
Task task = Task.Run(async () =>
{
for (int i = 0; i < 10; i++)
{
// 检查是否收到取消请求,若收到则抛出OperationCanceledException异常
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"打印当前迭代值:{i + 1}");
// 模拟异步操作延迟1秒
await Task.Delay(1000);
}
});
Console.WriteLine("按下任意按钮取消 Task");
// 阻塞程序,等待用户按下任意键
Console.ReadKey();
// 触发取消请求,通知所有关联的CancellationToken取消操作
cancellationTokenSource.Cancel();
try
{
// 等待任务完成,若任务被取消会在此处抛出异常
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("按下了任意按钮,Task 取消了,抛出OperationCanceledException异常");
}
finally
{
// 释放CancellationTokenSource占用的非托管资源
cancellationTokenSource.Dispose();
}
Console.WriteLine("程序结束");
}
}
}
取消机制原理
原生 Task 的取消通过协作式模式实现,核心组件为:
CancellationTokenSource
:取消指令发射器CancellationToken
:取消状态接收器ThrowIfCancellationRequested()
:取消检测方法
当调用Cancel()
时,所有关联的CancellationToken
会进入取消状态,但需要任务内部主动检查才能响应取消请求。
测试脚本解析
// 创建取消令牌源,用于控制异步操作的取消逻辑
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
// 从令牌源获取取消令牌,用于在异步操作中检测取消请求
CancellationToken cancellationToken = cancellationTokenSource.Token;
// 启动一个异步任务,使用匿名方法定义任务逻辑
Task task = Task.Run(async () =>
{
for (int i = 0; i < 10; i++)
{
// 检查是否收到取消请求,若收到则抛出OperationCanceledException异常
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"打印当前迭代值:{i + 1}");
// 模拟异步操作延迟1秒
await Task.Delay(1000);
}
});
取消执行流程
- 用户按下任意键触发
cts.Cancel()
- 令牌状态变为
IsCancellationRequested = true
- 循环中
ThrowIfCancellationRequested()
检测到取消状态 - 立即抛出
OperationCanceledException
中断任务
异常处理与资源释放
Console.WriteLine("按下任意按钮取消 Task");
// 阻塞程序,等待用户按下任意键
Console.ReadKey();
// 触发取消请求,通知所有关联的CancellationToken取消操作
cancellationTokenSource.Cancel();
try
{
// 等待任务完成,若任务被取消会在此处抛出异常
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("按下了任意按钮,Task 取消了,抛出OperationCanceledException异常");
}
finally
{
// 释放CancellationTokenSource占用的非托管资源
cancellationTokenSource.Dispose();
}
Console.WriteLine("程序结束");
运行结果
按下任意按钮取消 Task
打印当前迭代值:1
打印当前迭代值:2
打印当前迭代值:3
打印当前迭代值:4
打印当前迭代值:5
按下了任意按钮,Task 取消了,抛出OperationCanceledException异常
程序结束
进程已结束,退出代码为 0。
UniTask 取消机制详解
在之前的博文中,我们探讨了原生 Task
的取消机制。本文将重点介绍 UniTask 如何实现任务取消,并结合官方文档和示例代码进行说明。
取消操作原理
与原生 Task
相似,UniTask 也采用协作式取消机制。核心组件包括:
- CancellationTokenSource:用于发出取消指令。
- CancellationToken:用于接收取消状态。
- **ThrowIfCancellationRequested()**:用于检测取消请求。
当调用 CancellationTokenSource.Cancel()
时,所有关联的 CancellationToken
会进入取消状态。任务内部需要主动检查取消状态,以响应取消请求。
测试脚本
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
// 取消操作,大致原理同 Task 中的取消
// 通过 CancellationTokenSource 创建对应令牌 CancellationToken
// 通过 CancellationToken 与要取消的任务对应,通过 CancellationTokenSource 的 Cancel 方法设置 CancellationToken 标志位,任务取消,CancellationToken 抛出异常
// 捕获异常,任务停止
// async UniTaskVoid 与 async Void 比较
// async Void 是 C# 提供的,比较重
// 最好不要在 UniTask 中使用
public class Lesson09_取消操作 : MonoBehaviour
{
public GameObject ball;
public Button btnMove;
public Button btnCancel;
private CancellationTokenSource _cancellationTokenSource;
private CancellationToken _cancellationToken;
void Start()
{
// 创建一个新的 CancellationTokenSource 实例
_cancellationTokenSource = new CancellationTokenSource();
// 获取 CancellationTokenSource 对应的取消令牌
_cancellationToken = _cancellationTokenSource.Token;
// 为按钮添加点击事件监听器,使用 UniTask.UnityAction 避免使用 async void
// btnMove.onClick.AddListener(OnClickBtnMove);
// btnMove.onClick.AddListener(UniTask.UnityAction(async () => await OnClickBtnMove()));
btnMove.onClick.AddListener(UniTask.UnityAction(OnClickBtnMove));
btnCancel.onClick.AddListener(OnClickBtnCancel);
}
// private async void OnClickBtnMove()
// private async UniTask OnClickBtnMove()
private async UniTaskVoid OnClickBtnMove()
{
try
{
// 调用 Move 方法,传入球的 Transform 和取消令牌,等待移动任务完成
await Move(ball.transform, _cancellationToken);
}
catch (OperationCanceledException)
{
Debug.Log("捕获到OperationCanceledException异常");
}
}
private void OnClickBtnCancel()
{
// 取消并更新 Token
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token;
}
// 定义一个异步方法 Move,用于移动指定的 Transform 对象
// 接收一个 Transform 类型的参数 tf 表示要移动的对象,以及一个 CancellationToken 类型的参数 CancellationToken 用于取消操作
// 方法返回一个异步任务,任务完成后返回一个 int 类型的值
private async UniTask<int> Move(Transform ballTransform, CancellationToken token)
{
// 定义总移动时间为 5 秒
float totalTime = 5;
// 定义当前已经移动的时间,初始值为 0
float currentTime = 0;
// 当当前移动时间小于总移动时间时,持续执行循环
while (currentTime < totalTime)
{
// 累加当前移动时间,使用 Time.deltaTime 确保在不同帧率下移动速度一致
currentTime += Time.deltaTime;
// 移动 Transform 对象的位置,在 X 轴正方向上移动,移动速度与时间相关
ballTransform.position += new Vector3(1, 0, 0) * Time.deltaTime;
// 等待下一帧,并传入取消令牌,若取消则会抛出异常
await UniTask.NextFrame(token);
}
// 移动完成后返回 0
return 0;
}
}
关键点说明
按钮事件绑定:使用
UniTask.UnityAction
避免使用async void
,确保异步方法的正确执行和异常处理。btnMove.onClick.AddListener(UniTask.UnityAction(OnClickBtnMove));
任务取消:在
OnClickBtnCancel
方法中调用CancellationTokenSource.Cancel()
发出取消指令,并重新创建CancellationTokenSource
和CancellationToken
,以便后续使用。_cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); _cancellationTokenSource = new CancellationTokenSource(); _cancellationToken = _cancellationTokenSource.Token;
任务执行与取消检测:在
Move
方法中,使用await UniTask.NextFrame(token)
等待下一帧,并传入取消令牌。如果在此期间收到取消请求,将抛出OperationCanceledException
,任务可在catch
块中捕获并处理。await UniTask.NextFrame(token);
async void 与 async UniTaskVoid 对比
async void
是一个原生的 C# 任务系统,因此它不在 UniTask 系统上运行。也最好不要使用它,因为有可能导致无法捕获异常,难以进行任务管理等问题。async UniTaskVoid
是async UniTask
的轻量级版本,因为它没有等待完成并立即向UniTaskScheduler.UnobservedTaskException
报告错误。如果您不需要等待(即发即弃),那么使用UniTaskVoid
会更好。不幸的是,要解除警告,您需要在尾部添加Forget()
。
public async UniTaskVoid FireAndForgetMethod()
{
// do anything...
await UniTask.Yield();
}
public void Caller()
{
FireAndForgetMethod().Forget();
}
UniTask 也有Forget
方法,与UniTaskVoid
类似且效果相同。如果您完全不需要使用await
,那么使用UniTask
会更高效。
public async UniTask DoAsync()
{
// do anything...
await UniTask.Yield();
}
public void Caller()
{
DoAsync().Forget();
}
要使用注册到事件的异步 lambda,请不要使用async void
。您可以使用UniTask.Action
或 UniTask.UnityAction
来代替,这两者都通过async UniTaskVoid
lambda 来创建委托。
Action actEvent;
UnityAction unityEvent; // UGUI 特供
// 这样是不好的: async void
actEvent += async () => { };
unityEvent += async () => { };
// 这样是可以的: 通过 lamada 创建 Action
actEvent += UniTask.Action(async () => { await UniTask.Yield(); });
unityEvent += UniTask.UnityAction(async () => { await UniTask.Yield(); });
UniTaskVoid
也可以用在 MonoBehaviour 的Start
方法中。
class Sample : MonoBehaviour
{
async UniTaskVoid Start()
{
// 异步初始化代码。
}
}
返回值UniTask和UniTaskVoid
适合返回 UniTask
的场景
- 需要返回值:当异步操作需要返回一个结果时,应该使用
UniTask<T>
(T
是返回值的类型)。例如,异步从网络获取数据、从文件读取内容等,这些操作完成后通常会有一个结果需要传递给调用者。
public async UniTask<string> GetDataFromNetwork()
{
// 模拟网络请求
await UniTask.Delay(1000);
return "Some data from network";
}
public async void Caller()
{
string data = await GetDataFromNetwork();
Debug.Log(data);
}
在上述示例中,GetDataFromNetwork
方法异步获取数据并返回一个 string
类型的结果,调用者可以等待该任务完成并获取结果。
- 需要等待任务完成:如果调用者需要等待异步任务完成后再继续执行后续代码,那么使用
UniTask
是合适的。例如,在执行一系列依赖的异步操作时,需要确保前一个操作完成后再进行下一个操作。
public async UniTask DoStep1()
{
await UniTask.Delay(1000);
Debug.Log("Step 1 completed");
}
public async UniTask DoStep2()
{
await UniTask.Delay(1000);
Debug.Log("Step 2 completed");
}
public async void Caller()
{
await DoStep1();
await DoStep2();
Debug.Log("All steps completed");
}
在这个例子中,Caller
方法会依次等待 DoStep1
和 DoStep2
完成后再继续执行后续代码。
适合返回 UniTaskVoid
的场景
- 即发即弃的操作:当你发起一个异步操作,但不需要等待它完成,也不需要获取其返回值时,可以使用
UniTaskVoid
。例如,在游戏中播放一个音效或者显示一个短暂的提示信息,这些操作不需要调用者等待完成。
public async UniTaskVoid PlaySoundEffect()
{
await UniTask.Delay(1000);
Debug.Log("Sound effect played");
}
public void Caller()
{
PlaySoundEffect().Forget();
Debug.Log("Continuing other tasks...");
}
在这个示例中,Caller
方法调用 PlaySoundEffect
后不会等待音效播放完成,而是直接继续执行后续代码。
- 事件处理方法:在事件处理方法中,如果需要执行异步操作,使用
UniTaskVoid
可以避免使用async void
带来的问题。例如,在 Unity 中处理按钮点击事件时,可以使用UniTaskVoid
来执行异步逻辑。
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
public class ButtonHandler : MonoBehaviour
{
public Button myButton;
void Start()
{
myButton.onClick.AddListener(UniTask.UnityAction(OnButtonClick));
}
private async UniTaskVoid OnButtonClick()
{
await UniTask.Delay(1000);
Debug.Log("Button clicked and async operation completed");
}
}
在这个例子中,OnButtonClick
方法是一个按钮点击事件的处理方法,使用 UniTaskVoid
可以在其中执行异步操作,并且避免了 async void
的潜在问题。
CancelAfter延迟取消
using System.Threading;
using UnityEngine;
using Cysharp.Threading.Tasks;
using System;
// CancellationTokenSource 的 CancelAfter 方法
// ***时间后取消任务,可用于做超时处理
public class Lesson09_取消操作_延时取消 : MonoBehaviour
{
// 定义取消令牌源,用于控制异步任务的取消
private CancellationTokenSource _cancellationTokenSource;
// 定义取消令牌,用于在异步任务中检测取消请求
private CancellationToken _cancellationToken;
void Start()
{
// 创建一个新的取消令牌源实例
_cancellationTokenSource = new CancellationTokenSource();
// 启动一个异步任务,该任务将执行具体的测试逻辑
UniTaskVoid taskVoid = BeginTestCancelAfter();
// 设置在指定时间(这里是 1 秒)后触发取消请求,实现超时处理
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(1));
}
// 定义一个异步方法,用于测试延时取消功能
// 接收一个取消令牌作为参数,用于在任务执行过程中检测取消请求
// 方法返回一个异步任务,任务完成后返回一个 int 类型的值
async UniTask<int> TestCancelAfter(CancellationToken token)
{
// 循环 5 次,模拟一个持续的工作任务
for (int i = 0; i < 5; i++)
{
Debug.Log($"打印当前迭代值:{i + 1}");
// 等待 1 秒,并传入取消令牌,若取消则会抛出异常
await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: token);
}
// 任务正常完成后返回 0
return 0;
}
// 定义一个异步方法,用于开始测试取消功能
// 该方法返回一个 UniTaskVoid 类型,表示即发即弃的异步任务
async UniTaskVoid BeginTestCancelAfter()
{
try
{
// 调用 TestCancelAfter 方法,并传入取消令牌源的令牌
// 等待该异步任务完成
await TestCancelAfter(_cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
// 捕获到取消异常,打印信息
Debug.Log("捕获到取消异常OperationCanceledException");
}
}
}
- 注意:官方文档是这样说的
- CancellationTokenSouce.CancelAfter是一个原生的api。但是在 Unity 中你不应该使用它,因为它依赖于线程计时器。CancelAfterSlim是 UniTask 的扩展方法,它使用 PlayerLoop 代替。
- 如果您想将超时与其他cancellation一起使用,请使用CancellationTokenSource.CreateLinkedTokenSource.
- 结论:最好使用CancelAfterSlim
CreateLinkedTokenSource 多条件取消实现
var cancelToken = new CancellationTokenSource();
cancelButton.onClick.AddListener(()=>
{
cancelToken.Cancel(); // 点击按钮后取消。
});
var timeoutToken = new CancellationTokenSource();
timeoutToken.CancelAfterSlim(TimeSpan.FromSeconds(5)); // 设置5s超时。
try
{
// 链接 token
var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancelToken.Token, timeoutToken.Token);
await UnityWebRequest.Get("http://foo").SendWebRequest().WithCancellation(linkedTokenSource.Token);
}
catch (OperationCanceledException ex)
{
if (timeoutToken.IsCancellationRequested)
{
UnityEngine.Debug.Log("Timeout.");
}
else if (cancelToken.IsCancellationRequested)
{
UnityEngine.Debug.Log("Cancel clicked.");
}
}
GetCancellationTokenOnDestroy绑定GameObject取消
CancellationToken 可以由CancellationTokenSource或 MonoBehaviour 的GetCancellationTokenOnDestroy扩展方法创建。
// 这个CancellationTokenSource和this GameObject生命周期相同,当this GameObject Destroy的时候,就会执行Cancel
await UniTask.DelayFrame(1000, cancellationToken: this.GetCancellationTokenOnDestroy());
9.2 知识点代码
Program.cs
using System;
using System.Threading;
using System.Threading.Tasks;
namespace UniTask_Console
{
internal class Program
{
static async Task Main(string[] args)
{
// 创建取消令牌源,用于控制异步操作的取消逻辑
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
// 从令牌源获取取消令牌,用于在异步操作中检测取消请求
CancellationToken cancellationToken = cancellationTokenSource.Token;
// 启动一个异步任务,使用匿名方法定义任务逻辑
Task task = Task.Run(async () =>
{
for (int i = 0; i < 10; i++)
{
// 检查是否收到取消请求,若收到则抛出OperationCanceledException异常
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"打印当前迭代值:{i + 1}");
// 模拟异步操作延迟1秒
await Task.Delay(1000);
}
});
Console.WriteLine("按下任意按钮取消 Task");
// 阻塞程序,等待用户按下任意键
Console.ReadKey();
// 触发取消请求,通知所有关联的CancellationToken取消操作
cancellationTokenSource.Cancel();
try
{
// 等待任务完成,若任务被取消会在此处抛出异常
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("按下了任意按钮,Task 取消了,抛出OperationCanceledException异常");
}
finally
{
// 释放CancellationTokenSource占用的非托管资源
cancellationTokenSource.Dispose();
}
Console.WriteLine("程序结束");
}
}
}
Lesson09_取消操作.cs
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
using UnityEngine;
using UnityEngine.UI;
// 取消操作,大致原理同 Task 中的取消
// 通过 CancellationTokenSource 创建对应令牌 CancellationToken
// 通过 CancellationToken 与要取消的任务对应,通过 CancellationTokenSource 的 Cancel 方法设置 CancellationToken 标志位,任务取消,CancellationToken 抛出异常
// 捕获异常,任务停止
// async UniTaskVoid 与 async Void 比较
// async Void 是 C# 提供的,比较重
// 最好不要在 UniTask 中使用
public class Lesson09_取消操作 : MonoBehaviour
{
public GameObject ball;
public Button btnMove;
public Button btnCancel;
private CancellationTokenSource _cancellationTokenSource;
private CancellationToken _cancellationToken;
void Start()
{
// 创建一个新的 CancellationTokenSource 实例
_cancellationTokenSource = new CancellationTokenSource();
// 获取 CancellationTokenSource 对应的取消令牌
_cancellationToken = _cancellationTokenSource.Token;
// 为按钮添加点击事件监听器,使用 UniTask.UnityAction 避免使用 async void
// btnMove.onClick.AddListener(OnClickBtnMove);
// btnMove.onClick.AddListener(UniTask.UnityAction(async () => await OnClickBtnMove()));
btnMove.onClick.AddListener(UniTask.UnityAction(OnClickBtnMove));
btnCancel.onClick.AddListener(OnClickBtnCancel);
}
// private async void OnClickBtnMove()
// private async UniTask OnClickBtnMove()
private async UniTaskVoid OnClickBtnMove()
{
try
{
// 调用 Move 方法,传入球的 Transform 和取消令牌,等待移动任务完成
await Move(ball.transform, _cancellationToken);
}
catch (OperationCanceledException)
{
Debug.Log("捕获到OperationCanceledException异常");
}
}
private void OnClickBtnCancel()
{
// 取消并更新 Token
_cancellationTokenSource.Cancel();
_cancellationTokenSource.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
_cancellationToken = _cancellationTokenSource.Token;
}
// 定义一个异步方法 Move,用于移动指定的 Transform 对象
// 接收一个 Transform 类型的参数 tf 表示要移动的对象,以及一个 CancellationToken 类型的参数 CancellationToken 用于取消操作
// 方法返回一个异步任务,任务完成后返回一个 int 类型的值
private async UniTask<int> Move(Transform ballTransform, CancellationToken token)
{
// 定义总移动时间为 5 秒
float totalTime = 5;
// 定义当前已经移动的时间,初始值为 0
float currentTime = 0;
// 当当前移动时间小于总移动时间时,持续执行循环
while (currentTime < totalTime)
{
// 累加当前移动时间,使用 Time.deltaTime 确保在不同帧率下移动速度一致
currentTime += Time.deltaTime;
// 移动 Transform 对象的位置,在 X 轴正方向上移动,移动速度与时间相关
ballTransform.position += new Vector3(1, 0, 0) * Time.deltaTime;
// 等待下一帧,并传入取消令牌,若取消则会抛出异常
await UniTask.NextFrame(token);
}
// 移动完成后返回 0
return 0;
}
}
Lesson09_取消操作_延时取消.cs
using System.Threading;
using UnityEngine;
using Cysharp.Threading.Tasks;
using System;
// CancellationTokenSource 的 CancelAfter 方法
// ***时间后取消任务,可用于做超时处理
public class Lesson09_取消操作_延时取消 : MonoBehaviour
{
// 定义取消令牌源,用于控制异步任务的取消
private CancellationTokenSource _cancellationTokenSource;
// 定义取消令牌,用于在异步任务中检测取消请求
private CancellationToken _cancellationToken;
void Start()
{
// 创建一个新的取消令牌源实例
_cancellationTokenSource = new CancellationTokenSource();
// 启动一个异步任务,该任务将执行具体的测试逻辑
UniTaskVoid taskVoid = BeginTestCancelAfter();
// 设置在指定时间(这里是 1 秒)后触发取消请求,实现超时处理
_cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(1));
}
// 定义一个异步方法,用于测试延时取消功能
// 接收一个取消令牌作为参数,用于在任务执行过程中检测取消请求
// 方法返回一个异步任务,任务完成后返回一个 int 类型的值
async UniTask<int> TestCancelAfter(CancellationToken token)
{
// 循环 5 次,模拟一个持续的工作任务
for (int i = 0; i < 5; i++)
{
Debug.Log($"打印当前迭代值:{i + 1}");
// 等待 1 秒,并传入取消令牌,若取消则会抛出异常
await UniTask.Delay(TimeSpan.FromSeconds(1), cancellationToken: token);
}
// 任务正常完成后返回 0
return 0;
}
// 定义一个异步方法,用于开始测试取消功能
// 该方法返回一个 UniTaskVoid 类型,表示即发即弃的异步任务
async UniTaskVoid BeginTestCancelAfter()
{
try
{
// 调用 TestCancelAfter 方法,并传入取消令牌源的令牌
// 等待该异步任务完成
await TestCancelAfter(_cancellationTokenSource.Token);
}
catch (OperationCanceledException)
{
// 捕获到取消异常,打印信息
Debug.Log("捕获到取消异常OperationCanceledException");
}
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com