10.UniTask超时处理

  1. 10.超时处理
    1. 10.1 知识点
      1. 测试脚本
      2. 性能优化详解
      3. 异常处理策略
    2. 10.2 知识点代码
      1. Lesson10_超时处理.cs

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;
    }
}

性能优化详解

  1. CancelAfterSlim

    • 基于 Unity 的 PlayerLoop 实现,避免传统 CancelAfter 的线程阻塞问题
    • 示例:cts.CancelAfterSlim(TimeSpan.FromSeconds(2))
    功能特性 原生实现 (Task) UniTask 优化方案
    超时触发 CancelAfter(TimeSpan) CancelAfterSlim(TimeSpan)
    异常处理 try/catch 抛出异常 SuppressCancellationThrow 返回元组
    计时系统 线程池计时器 Unity PlayerLoop 驱动
    资源消耗 高(线程池占用) 低(无额外线程开销)
  2. 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();
}
  1. 令牌复用机制
    使用 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传递给该方法,它将从任务内部执行,因此可以停止正在运行的任务。

异常处理策略

  1. 全局异常监听
    当检测到取消时,所有方法都会向上游抛出并传播OperationCanceledException。当异常(不限于OperationCanceledException)没有在异步方法中处理时,它将被传播到UniTaskScheduler.UnobservedTaskException。默认情况下,将接收到的未处理异常作为一般异常写入日志。可以使用UniTaskScheduler.UnobservedExceptionWriteLogType更改日志级别。若想对接收到未处理异常时的处理进行自定义,请为UniTaskScheduler.UnobservedTaskException设置一个委托
UniTaskScheduler.UnobservedTaskException += ex =>
{
    Debug.LogError($"未处理异常: {ex.Message}");
};
  1. 异常过滤处理
    只想处理异常,忽略取消操作(让其传播到全局处理 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

×

喜欢就点赞,疼爱就打赏