10.超时处理
10.1 知识点
测试脚本
using Cysharp.Threading.Tasks;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
//UniTask 官方说明 ( 之前只是没讲到 所以没说 尽量遵守)
//1.关于async void 与async UniTaskVoid
//2.考虑性能的话 请使用 UniTask.SuppressCancellationThrow 返回值 (bool IsCanceled, T Result)
//3.超时处理本质上是取消 Unity中建议使用CancellationTokenSouce.CancelAfterSlim(TimeSpan)
//CancellationTokenSouce.CancelAfter是c#提供的原生api 在Unity中不该使用
//Forget 方法 UniTask提供 同步方法中调用异步方法 不想await 又不想有警告 可用Forget
public class Ten : MonoBehaviour
{
public Button btn1;
public Button btn2;
public Text text1;
public Text text2;
public String url1 = "https://www.baidu.com/";
public String url2 = "https://www.google.com/";
void Start()
{
btn1.onClick.AddListener(() => { OnClickTest(url1, text1).Forget(); });
btn2.onClick.AddListener(() => { OnClickTest(url2, text2).Forget(); });
}
public async UniTask<string> VisitWeb(string url, float timeout)
{
CancellationTokenSource cts = new CancellationTokenSource();
//在指定时间后取消任务
cts.CancelAfterSlim(TimeSpan.FromSeconds(timeout));
//没有抛出异常 通过返回值判断任务是否取消
//返回值 bool T
//SuppressCancellationThrow 抛出异常的第二种方式
//发送网络请求 并且会有一个取消异常
// 返回值 第一个参数 bool 是否取消 第二个参数结果
var (failed, result) = await UnityWebRequest.Get(url).SendWebRequest().WithCancellation(cts.Token).SuppressCancellationThrow();
if (!failed)
{
return result.downloadHandler.text.Substring(0, 20);
}
//如果等到指定时间任务取消了 说明等待时间内都没连上
return "超时";
}
private async UniTaskVoid OnClickTest(string url, Text _text)
{
var res = await VisitWeb(url, 2);
_text.text = res;
}
}
性能优化详解
CancelAfterSlim
- 基于 Unity 的
PlayerLoop
实现,避免传统CancelAfter
的线程阻塞问题 - 示例:
cts.CancelAfterSlim(TimeSpan.FromSeconds(2))
功能特性 原生实现 ( Task
)UniTask 优化方案 超时触发 CancelAfter(TimeSpan)
CancelAfterSlim(TimeSpan)
异常处理 try/catch
抛出异常SuppressCancellationThrow
返回元组计时系统 线程池计时器 Unity PlayerLoop 驱动 资源消耗 高(线程池占用) 低(无额外线程开销) - 基于 Unity 的
SuppressCancellationThrow异常处理优化
将取消操作转换为布尔值判断,避免异常堆栈开销
// 传统方式(性能损耗)
// 取消异步 UniTask 方法中的行为,请手动抛出OperationCanceledException。
public async UniTask<int> FooAsync()
{
await UniTask.Yield();
throw new OperationCanceledException();
}
// 优化方式(推荐)
// 使用UniTask.SuppressCancellationThrow以避免抛出 OperationCanceledException 。它将返回(bool IsCanceled, T Result)而不是抛出异常。
var (isCanceled, _) = await UniTask.DelayFrame(10, cancellationToken: cts.Token).SuppressCancellationThrow();
if (isCanceled)
{
HandleTimeout();
}
- 令牌复用机制
使用 UniTask 的TimeoutController进行优化,减少每次调用异步方法时用于超时的 CancellationTokenSource 的堆内存分配
TimeoutController timeoutController = new TimeoutController(); // 提前创建好,以便复用。
async UniTask FooAsync()
{
try
{
// 您可以通过 timeoutController.Timeout(TimeSpan) 把超时设置传递到 cancellationToken。
await UnityWebRequest.Get("http://foo").SendWebRequest()
.WithCancellation(timeoutController.Timeout(TimeSpan.FromSeconds(5)));
timeoutController.Reset(); // 当 await 完成后调用 Reset(停止超时计时器,并准备下一次复用)。
}
catch (OperationCanceledException ex)
{
if (timeoutController.IsTimeout())
{
UnityEngine.Debug.Log("timeout");
}
}
}
使用new TimeoutController(CancellationToken),让超时结合其他取消源一起使用
TimeoutController timeoutController;
CancellationTokenSource clickCancelSource;
void Start()
{
this.clickCancelSource = new CancellationTokenSource();
this.timeoutController = new TimeoutController(clickCancelSource);
}
注意:UniTask 有.Timeout,.TimeoutWithoutException方法,但如果可以的话,尽量不要使用这些方法,请传递CancellationToken。因为.Timeout是在任务外部执行,所以无法停止超时任务。.Timeout意味着超时后忽略结果。如果您将一个CancellationToken传递给该方法,它将从任务内部执行,因此可以停止正在运行的任务。
异常处理策略
- 全局异常监听
当检测到取消时,所有方法都会向上游抛出并传播OperationCanceledException。当异常(不限于OperationCanceledException)没有在异步方法中处理时,它将被传播到UniTaskScheduler.UnobservedTaskException。默认情况下,将接收到的未处理异常作为一般异常写入日志。可以使用UniTaskScheduler.UnobservedExceptionWriteLogType更改日志级别。若想对接收到未处理异常时的处理进行自定义,请为UniTaskScheduler.UnobservedTaskException设置一个委托
UniTaskScheduler.UnobservedTaskException += ex =>
{
Debug.LogError($"未处理异常: {ex.Message}");
};
- 异常过滤处理
只想处理异常,忽略取消操作(让其传播到全局处理 cancellation 的地方),使用异常过滤器。
public async UniTask<int> BarAsync()
{
try
{
var x = await FooAsync();
return x * 2;
}
catch (Exception ex) when (!(ex is OperationCanceledException)) // 在 C# 9.0 下改成 when (ex is not OperationCanceledException)
{
return -1;
}
}
10.2 知识点代码
Lesson10_超时处理.cs
using Cysharp.Threading.Tasks;
using System;
using System.Threading;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;
// UniTask 官方说明(此处为开发者备注,说明注意事项:)
// 1. 关于 async void 与 async UniTaskVoid 的使用。
// 2. 为了性能考虑,建议使用 UniTask.SuppressCancellationThrow,返回值为 (bool IsCanceled, T Result)。
// 3. 超时处理本质上是取消任务,Unity中建议使用 CancellationTokenSource.CancelAfterSlim(TimeSpan);
// CancellationTokenSource.CancelAfter 是 C# 提供的原生 API,在 Unity 中不推荐使用。
// Forget 方法说明:UniTask 提供的 Forget 方法用于在同步方法中调用异步方法,避免未 await 导致的警告。
public class Lesson10_超时处理 : MonoBehaviour
{
// 按钮与文本组件,用于显示请求结果
public Button btn1;
public Button btn2;
public Text text1;
public Text text2;
// 请求的网址
public String url1 = "https://www.baidu.com/";
public String url2 = "https://www.google.com/";
// Unity 生命周期方法,初始化时添加按钮点击监听事件
void Start()
{
// 为 btn1 按钮添加点击事件,调用 OnClickTest 方法并使用 Forget 忽略返回的 UniTask 警告
btn1.onClick.AddListener(() => { OnClickTest(url1, text1).Forget(); });
// 为 btn2 按钮添加点击事件
btn2.onClick.AddListener(() => { OnClickTest(url2, text2).Forget(); });
}
/// <summary>
/// 按钮点击事件处理方法,调用 VisitWeb 方法获取请求结果并更新界面文本。
/// </summary>
/// <param name="url">要访问的 URL 地址</param>
/// <param name="_text">显示结果的 Text 组件</param>
private async UniTaskVoid OnClickTest(string url, Text _text)
{
// 调用 VisitWeb 方法,超时时间设置为 2 秒,并等待返回结果
string stringResult = await VisitWeb(url, 2);
// 将获取的结果显示在对应的 Text 组件上
_text.text = stringResult;
}
/// <summary>
/// 异步访问指定 URL 的方法,并在指定超时时间内返回结果。a
/// </summary>
/// <param name="url">要请求的网页地址</param>
/// <param name="timeout">超时时间(单位:秒)</param>
/// <returns>
/// 如果请求成功,返回网页文本的前 20 个字符;
/// 如果超时,则返回 "超时" 字符串。
/// </returns>
public async UniTask<string> VisitWeb(string url, float timeout)
{
// 创建用于任务取消的 CancellationTokenSource
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
// 在指定的超时时间后自动取消任务(使用 CancelAfterSlim,适用于 Unity 环境)
cancellationTokenSource.CancelAfterSlim(TimeSpan.FromSeconds(timeout));
// 发送 GET 请求,并传入取消令牌,使用 SuppressCancellationThrow 处理取消异常,
// 返回值为元组:(isFailed, unityWebRequestResult),其中 isFailed 为是否取消,unityWebRequestResult 为请求结果
var (isFailed, unityWebRequestResult) = await UnityWebRequest.Get(url).SendWebRequest()
.WithCancellation(cancellationTokenSource.Token)
.SuppressCancellationThrow();
// 如果请求未被取消,则返回网页内容的前 20 个字符
if (!isFailed)
{
return unityWebRequestResult.downloadHandler.text.Substring(0, 20);
}
// 如果任务在指定时间内未能完成,则认为请求超时,返回 "超时"
return "超时";
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com