• 记一次.net core 异步线程设置超时时间


    前言:

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

    需求背景:

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

    需求提炼:

    • A任务必须在指定时间内完成,否则任务失败
    • A任务依赖B,C任务,B,C可以并行,任何一个失败,则A任务失败
    • A任务在超时时间内,是否需求记录子任务执行详情(根据业务需求来定)

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

    测试截图

    image

    image

    image

    image

    搞定收工

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

    我们能做到在3s内响应结果,也算基本上满足了需求,那超时的子任务,是否会继续执行呢?
    仔细看代码,就算超时,也是停止的Task.Delay() 这个线程,与业务线程没有半毛钱关系,那业务线程肯定会继续执行
    眼尖的同学已经看到最后一张图,B任务执行了3.0076088s,按道理B任务是已经超时了,这段话是不会输出的,那如果我让主线程晚点退出,那超时的子线程是否能正常执行, //await Task.Delay(3000); 将这段代码取消注释,再来观看结果
    image

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

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

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

  • 相关阅读:
    检测单链表是否含有环
    巧用正则和document.location.search获取URL参数的值
    网站前端性能优化
    Ubuntu 10.10 wubi安装错误处理
    SQL中随机数函数rand()简介
    C#实现智能提示(提示补全)功能
    重装系统使之svn再次复活
    MongoDB安装
    mongodb查询
    解决iframe中跨域session丢失的问题
  • 原文地址:https://www.cnblogs.com/tibos/p/15878657.html
Copyright © 2020-2023  润新知