当前位置:主页 > 软件编程 > .NET代码 >

一次.net core异步线程设置超时时间的实战记录

时间:2022-07-15 08:36:05 | 栏目:.NET代码 | 点击:

前言:

刷帖子看到一篇 Go 记录一次groutine通信与context控制 看了一下需求背景,挺有意思的,琢磨了下.net core下的实现

需求背景:

项目中需要定期执行任务A来做一些辅助的工作,A的执行需要在超时时间内完成,如果本次执行超时了,那就不对本次的执行结果进行处理(即放弃这次执行)。同时A又依赖B,C两个子任务的执行结果。B, C之间相互独立,可以并行的执行。但无论B,C哪一个执行失败或超时都会导致本次任务执行失败。

需求提炼:

.net里设置超时的 Task

public static class TaskHelper
{

    // 有返回值
    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;  // Very important in order to propagate exceptions
            }
            else
            {
                throw new TimeoutException("The operation has timed out.");
            }
        }
    }

    // 无返回值
    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;  // Very important in order to propagate exceptions
            }
            else
            {
                throw new TimeoutException("The operation has timed out.");
            }
        }
    }
}

这里参考资料,写了个拓展方法,主要用到CancellationTokenSource 与 Task.WhenAny

可以参考 C#中CancellationToken和CancellationTokenSource用法

可以参考 Task.WhenAny 方法

这里需要特别注意,在异步操作里,如果异步已经执行了,再执行取消时无效的,这是就需要我们自己在异步委托中检测了

知道了这两个函数的作用,这段代码就很好理解了,通过Task.WhenAny返回最先完成的任务,如果是业务任务先完成,则调用timeoutCancellationTokenSource.Cancel()终止超时任务,等待业务任务结果,反之则直接抛出timeout异常

测试代码

    [TestMethod]
    public async Task TestMethod1()
    {
        //A 任务必须在指定时间内完成,否则任务失败
        //A 任务依赖B,C任务,B,C可以并行,任何一个失败,则A任务失败
        //A任务
        try
        {
            //有效时间3s
            var timeOut = TimeSpan.FromSeconds(3);
            await Task.Run(async () =>
            {
                List<Task<(string, bool)>> tasks = new List<Task<(string, bool)>>();
                //B任务
                tasks.Add(Task.Run(async () =>
                {
                    return ("B", await TestTask("B"));
                }).TimeoutAfter(timeOut));

                //C任务
                tasks.Add(Task.Run(async () =>
                {
                    return ("C", await TestTask("C"));
                }).TimeoutAfter(timeOut));

                var res = await Task.WhenAll(tasks);

                //两个任务,任何一个失败,则A任务失败
                foreach (var item in res)
                {
                    Console.WriteLine(item);
                }

            }).TimeoutAfter(timeOut);

        }
        catch (Exception ex)
        {
            Console.WriteLine("A任务执行超时了");
        }

        //await Task.Delay(3000);


    }

    public async Task<bool> TestTask(string name) 
    {
        var startTime = DateTime.Now;
        Console.WriteLine($"{startTime}---->{name}任务开始执行");
        //随机堵塞1-5s
        var t = new Random().Next(1, 5);
        await Task.Delay(t * 1000);

        var endTime = DateTime.Now; ;
        var time = (endTime - startTime).TotalSeconds;

        //随机数,模拟业务是否成功
        var res = new Random().Next(1, 10);
        Console.WriteLine($"{endTime}---->{name}任务执行完毕,耗时{time} s");
        return res <= 7;
    }

测试截图

搞定收工

故事在这里就结束了吗? 显然没有,这么简单也没必要水一篇博客了

我们能做到在3s内响应结果,也算基本上满足了需求,那超时的子任务,是否会继续执行呢?

仔细看代码,就算超时,也是停止的Task.Delay() 这个线程,与业务线程没有半毛钱关系,那业务线程肯定会继续执行

眼尖的同学已经看到最后一张图,B任务执行了3.0076088s,按道理B任务是已经超时了,这段话是不会输出的,那如果我让主线程晚点退出,那超时的子线程是否能正常执行, //await Task.Delay(3000); 将这段代码取消注释,再来观看结果

有没有一种被欺骗的感觉,写了一个假的超时时间,哈哈哈哈.....

这里需要特别注意,在异步操作里,如果异步已经执行了,再执行取消时无效的,这是就需要我们自己在异步委托中检测了

如果写了一个死循环的task,那后果将不堪设想,这个时候,就需要慎重了,在使用多线程取消令牌的时候,除了需要执行Cancel()方法,还需要在子任务内自己捕获CancellationTokenSource.Token.ThrowIfCancellationRequested()

总结

您可能感兴趣的文章:

相关文章