Task 超时处理

一、使用 Task.WaitAll

public static async Task<string> GetDataAsync()
{
    var data = await GetData();
    return data;
}

private static Task<string> GetData()
{
    var task = new Task<string>(() =>
    {
        Thread.Sleep(1000);
        return "hello";
    });

    task.Start(); //开始运行

    //判断超时
    var completed = Task.WaitAll(new Task[]{ task }, TimeSpan.FromSeconds(3));
    if (!completed)
    {
        throw new Exception("timeout");
    }

    return task;
}

Task.WaitAll 有个缺点,它是同步的,会阻塞当前 或 UI 线程。

二、使用 Task.WhenAny 和 CancellationTokenSource

Task.WhenAny返回最先完成的任务对象,CancellationTokenSource可以用来取消未完成任务。

public static async Task<string> GetDataAsync()
{
    var data = await GetData(5);
    return data;
}

private static async Task<string> GetData(int timeout)
{
    //工作任务
    var workTask = Task.Run(() =>
    {
        Thread.Sleep(20000);
        return "hello";
    });

    using (var cts = new CancellationTokenSource())
    {
        //工作任务 + 超时任务 哪个先完成
        var completedTask = await Task.WhenAny(workTask, Task.Delay(timeout * 1000, cts.Token));

        //如果工作任务先完成
        if (completedTask == workTask)
        {
            cts.Cancel(); //取消 Task.Delay 任务
            return await workTask;
        }
        else
        {
            //否则超时
            throw new TimeoutException("The operation has timed out.");
        }
    }
}

这里借助了 Task.Delay做了任务执行时间对比,如果工作任务晚于超时任务完成,即工作超时。
但是这样写会有一个问题,就是每次做超时都会写这段代码,可以考虑改为扩展方法。

Task无返回值

public static async Task TimeoutAfter(this Task task, TimeSpan timeout)
{
    using (var timeoutCancellationTokenSource = new CancellationTokenSource())
    {
        var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
        if (completedTask == task)
        {
            timeoutCancellationTokenSource.Cancel();
            await task;  
        }
        else
        {
            throw new TimeoutException("The operation has timed out.");
        }
    }
}

Task有返回值

public static async Task<TResult> TimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout)
{
    using (var timeoutCancellationTokenSource = new CancellationTokenSource())
    {
        var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
        if (completedTask == task)
        {
            timeoutCancellationTokenSource.Cancel();
            return await task; 
        }
        else
        {
            throw new TimeoutException("The operation has timed out.");
        }
    }
}

测试代码

public static async Task<string> GetDataAsync()
{
    var data = await GetData(5);
    return data;
}

private static async Task<string> GetData(int timeout)
{
    //工作任务
    var workTask = Task.Run(() =>
    {
        Thread.Sleep(2000);
        return "hello";
    });
    
	//5秒后超时
    return await workTask.TimeoutAfter(TimeSpan.FromSeconds(5));
}

但是这样还有一个问题,虽然能检测到超时,但 workTask 任务还是会继续执行到完毕。要想超时就取消操作,这就要为 workTask 也设置 CancellationTokenSource

三、仅使用 CancellationTokenSource 处理超时

虽然CancellationTokenSource只是管理取消任务的令牌,本身没有超时检测的能力。
但是我们可以利用它的延迟功能达到: 延迟一定的时间后(认为超时了)取消任务

先看下面的代码,是永远不会检测到超时的

public static async Task<string> GetDataAsync()
{
    var data = await GetData(5);
    return data;
}

private static async Task<string> GetData(int timeout)
{
    using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout)))
    {
        //工作任务
        var workTask = Task.Run(() =>
        {
            Thread.Sleep(8000);
            return "hello";
        }, cts.Token);

        return await workTask;
    }
}

原因是,我们只是延迟 5 秒来让 CancellationTokenSource 取消操作,虽然它被用到 Task 第二个参数里,但是我们的工作内容里并没有处理取消操作的代码,只用了 Thread.Sleep(8000); 这种同步方法不支持取消,任务还是会一直执行下去。

Task 任务要能被取消,就要在工作内容中使用支持取消操作的代码,例如IsCancellationRequestedThrowIfCancellationRequested()

//工作任务
var workTask = Task.Run(() =>
{
    for (int i = 0; i < 10; i++)
    {
        if (cts.Token.IsCancellationRequested)
        {
            return "timeout";
        }

        Thread.Sleep(1000);
    }

    return "hello";
}, cts.Token);
//工作任务
var workTask = Task.Run(() =>
{
    try
    {
        for (int i = 0; i < 10; i++)
        {
            cts.Token.ThrowIfCancellationRequested();
            Thread.Sleep(1000);
        }
    }
    catch (Exception e)
    {
        return "timeout";
    }

    return "hello";
}, cts.Token);

下面这段代码是核心,它能延迟 timeout 秒后,自动执行 Cancel() 命令。

var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeout))

通过 CancellationTokenSource 的延迟取消功能,就能为 Task 赋予超时检测能力。

四、TaskCompletionSource 处理超时

很多时候,Task 是结合 TaskCompletionSource 使用的,本质上还是处理Task 的超时,只不过我们要多处理一下TaskCompletionSource 的结果。可以使用 TaskCompletionSource 的 SetExceptionSetCanceled 来处理。

实现方式:还是使用Task.WhenAny(看谁先完成) + CancellationTokenSource(取消未完成的任务)。

public static async Task<string> GetDataAsync()
{
    var data = await GetData(5);
    return data;
}

private static async Task<string> GetData(int timeout)
{
    var tcs = new TaskCompletionSource<string>();
    
    using (var cts = new CancellationTokenSource())
    {
        var workTask = Task.Run(() =>
        {
            //模拟耗时
            for (int i = 0; i < 10; i++)
            {
                if (cts.IsCancellationRequested)
                {
                	//tcs.SetCanceled(); //设置了 SetException 就可以不用设置 SetCanceled
                    return;
                }
                Thread.Sleep(1000); 
            }
            
            tcs.SetResult("工作完成"); //正常执行完成
            
        }, cts.Token);

        var completedTask = await Task.WhenAny(workTask, Task.Delay(TimeSpan.FromSeconds(timeout), cts.Token));

        if (completedTask == workTask)
        {
            cts.Cancel(); //取消 Task.Delay 操作
        }
        else
        {
            cts.Cancel(); //取消 workTask 操作
            tcs.SetException(new TimeoutException("超时啦")); //设置超时异常
        }
    }

    return await tcs.Task;
}

未超时 SetResult,超时后 SetException() 或 SetCanceled()。

如果不清楚何时会触发 SetResult 方法,可以直接比较 tcs.TaskTask.Delay

public static async Task<string> GetDataAsync()
{
    var data = await GetData(5);
    return data;
}

private static async Task<string> GetData(int timeout)
{
    var tcs = new TaskCompletionSource<string>();

    //模拟外部耗时
    Task.Run(() =>
    {
        Thread.Sleep(4000);
        tcs.SetResult("完成");
    });

    using (var cts = new CancellationTokenSource())
    {
        var completed = await Task.WhenAny(tcs.Task, Task.Delay(3000, cts.Token));
        if (completed == tcs.Task)
        {
            cts.Cancel();
            return await tcs.Task;
        }
        else
        {
            throw new Exception("timeout");
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值