C# async/await任务超时处理的实现

 更新时间:2023年02月06日 10:06:44   作者:熊思宇  
本文主要介绍了C# async/await任务超时处理的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

一、需求

在之前的帖子中,介绍了 async / await 的用法,那么新的问题又来了,如果调用一个异步方法后,一直不给返回值结果怎么办呢?这就涉及到怎么取消任务了。添加一个任务后,如果固定时间内没用返回结果,那么就取消执行,并且在多个任务同时执行的时候,依然按顺序来执行,这就是本文章要实现的功能。

二、Task取消任务

先介绍一下 Task 结束任务的传统用法。在 C# 以前的2.0 等版本中,线程是可以强制终止的,到了后面,就不允许强制去结束线程的,后面微软就提供了一个 CancellationTokenSource 相关的接口开源取消任务。于是我搜索了大量的帖子,一看全是各种抄袭,相互抄来抄去的,搞的我真的火冒三丈,完全是浪费时间,那么总结取消的方法如下:

namespace 取消任务3
{
    internal class Program
    {
        static CancellationTokenSource source = new CancellationTokenSource();
 
        static void Main(string[] args)
        {
            Task.Run(() =>
            {
                for (int i = 0; i < 10; i++)
                {
                    Thread.Sleep(100);
                    Console.WriteLine("oh my god");
                    source.Token.ThrowIfCancellationRequested();
                }
            }, source.Token);
 
            Thread.Sleep(2000);
            Console.WriteLine("取消任务");
            source.Cancel();
 
            Console.ReadKey();
        }
    }
}

运行:

这个可以取消任务是吧,那下面换一个写法:

namespace 取消任务3
{
    internal class Program
    {
        static CancellationTokenSource source = new CancellationTokenSource();
 
        static void Main(string[] args)
        {
            Task.Run(() =>
            {
                Thread.Sleep(3000);
                Console.WriteLine("oh my god");
                source.Token.ThrowIfCancellationRequested();
            }, source.Token);
 
            Thread.Sleep(1000);
            Console.WriteLine("取消任务");
            source.Cancel();
 
            Console.ReadKey();
        }
    }
}

任务中等待三秒,我在等待一秒后取消任务,看看结果:

这回就不管用了,任务明明取消了,但结果依然执行了。

根据他们写的例子,可以总结一点,就是要想取消任务,在任务中,必须加入 for 或者 while 循环,并且在下一轮循环中,执行到 source.Token.ThrowIfCancellationRequested() 这句才能取消任务。

换个写法,如果非得用 for 或者 while 循环这样的语法才能取消任务,我在 while 循环中加入一个判断,如果等于 true,直接跳出循环,这不也是中断了任务,所以说 CancellationTokenSource 真的意义不大

namespace 取消任务4
{
    internal class Program
    {
        static void Main(string[] args)
        {
            bool isOut = false;
 
            var task1 = Task.Run(() =>
            {
                for (int i = 0; i < 100; i++)
                {
                    if (isOut) return;
 
                    Console.WriteLine("执行中" + i);
                    Thread.Sleep(500);
                }
            });
 
            Thread.Sleep(2000);
            Console.WriteLine("取消任务");
            isOut= true;
 
            Console.ReadKey();
        }
    }
}

运行:

三、Task取消任务的回调

取消任务也是可以加入回调的,如下:

namespace 取消任务2
{
    internal class Program
    {
        static CancellationTokenSource source = new CancellationTokenSource();
 
        static void Main(string[] args)
        {
            var task1 = Task.Run(() =>
            {
                for (int i = 0; i < 100; i++)
                {
                    source.Token.ThrowIfCancellationRequested();
                    Console.WriteLine("执行中" + i);
                    Thread.Sleep(500);
                }
            }, source.Token);
 
            //在指定的毫秒数后取消task执行
            source.CancelAfter(2 * 1000);
 
            //取消任务后的回调
            source.Token.Register(() =>
            {
                //不延迟会获取不到正确的状态
                Thread.Sleep(50);
                Console.WriteLine("task1状态:" + task1.Status);
                Console.WriteLine("IsFaulted状态:" + task1.IsFaulted);//由于未处理的异常,任务已完成。
                Console.WriteLine("IsCompleted状态:" + task1.IsCompleted);//获取一个值,该值指示任务是否已完成。
            });
 
            Console.ReadKey();
        }
    }
}

 运行:

四、Task超时处理的实现

在上面的介绍中可以看到,Task取消任务传统的用法并不好用,必须在里面加上条件判断,如果满足条件就跳出 for 或者 while 循环,达到方法执行完成的目的,而并不是真的终止了任务。

那么这么需求要如何去完成呢,微软官方也提供了一个叫 Task.WhenAny 接口,可以实现这个功能,下面就看看如何实现的。

新建一个基于 .Net6 的 Winform 项目,新建一个脚本 Lib.cs

namespace Utils
{
    public static class Lib
    {
 
        public static async Task<bool> TryWithTimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout, Action<TResult>? successor = null)
        {
            //用于取消任务
            using CancellationTokenSource timeoutCancellationTokenSource = new CancellationTokenSource();
 
   
            //WhenAny 等待所有任务结束,这里加入了超时时间
            //ConfigureAwait 配置用来等待 任务1的警报,返回值可以获取到改任务
            Task? completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token)).ConfigureAwait(continueOnCapturedContext: false);
 
 
            //如果当前任务完成了,并且匹配
            if (completedTask == task)
            {
                //取消任务
                timeoutCancellationTokenSource.Cancel();
 
                //得到任务返回结果
                var result = await task.ConfigureAwait(continueOnCapturedContext: false);
 
                //执行回调
                if(successor != null) 
                    successor(result);
 
                return true;
            }
            else //任务超时
                return false;
        }
    }
}

界面如下,就几个按钮

代码:

using Utils;
 
namespace 异步编程
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
 
        private void button1_Click(object sender, EventArgs e)
        {
            Test1();
        }
 
        private void button2_Click(object sender, EventArgs e)
        {
            Test2();
        }
 
        private void Button_ClearConsole_Click(object sender, EventArgs e)
        {
            Console.Clear();
        }
 
        private async void Test1()
        {
            var task1 = Task.Run(() =>
            {
                Thread.Sleep(3000);
                return "task1";
            });
            bool res1 = await task1.TryWithTimeoutAfter(TimeSpan.FromSeconds(4), (string msg) =>
            {
                Console.WriteLine("-----------------------task1回调:" + msg);
            });
 
            string isTimeout1 = res1 == true ? "没超时" : "超时";
            Console.WriteLine("任务1:" + isTimeout1);
 
 
            var task2 = Task.Run(() =>
            {
                Thread.Sleep(3000);
                return "task2";
            });
 
            bool res2 = await task2.TryWithTimeoutAfter(TimeSpan.FromSeconds(4), (string msg) =>
            {
                Console.WriteLine("-----------------------task2回调:" + msg);
            });
 
            string isTimeout2 = res2 == true ? "没超时" : "超时";
            Console.WriteLine("任务2:" + isTimeout2);
        }
 
        private async void Test2()
        {
            var task3 = Task.Run(() =>
            {
                Thread.Sleep(3000);
                return "task3";
            });
 
            bool res3 = await task3.TryWithTimeoutAfter(TimeSpan.FromSeconds(4), (string msg) =>
            {
                Console.WriteLine("-----------------------task3回调:" + msg);
            });
 
            string isTimeou3 = res3 == true ? "没超时" : "超时";
            Console.WriteLine("任务2:" + isTimeou3);
        }
    }
}

按钮1和按钮2 方法里有三个异步方法,超时时间都是4秒,也就是说,如果方法在4秒之内没有返回值则为失败。

分别点击按钮1,按钮2

在按钮1方法里有两个异步方法,异步方法1执行完成后,才能执行异步方法2,所以异步方法2要比异步方法3更慢一些。

下面就将三个异步方法的超时时间改为1秒,看看效果:

返回超时,而且任务也没有执行,这样就实现了我们的想要的效果了。 

五、Task.WhenAny 的异常

在一系列的测试中,我发现了 Task.WhenAny 这个接口在 .Net6 的控制台项目的异常之处,执行一次是正常的,如果在一个方法内同时执行多次,返回结果就开始乱了,在 Winform 项目中是没有这种事的,下面开始演示。

新建一个基于 .Net6 的控制台项目, 将上面的 Lib.cs 代码复制到项目中来。

代码:

using Utils;
 
namespace 异步编程2
{
    internal class Program
    {
        static void Main(string[] args)
        {
            AwaitReturnValue();
 
            Console.ReadKey();
        }
 
        public static async void AwaitReturnValue()
        {
            var task1 = Task.Run(() =>
            {
                Thread.Sleep(3000);
                return "task1";
            });
 
 
            for (int i = 0; i < 10; i++)
            {
                bool res1 = await task1.TryWithTimeoutAfter(TimeSpan.FromSeconds(1), (string msg) =>
                {
                    Console.WriteLine("task1回调:" + msg);
                });
 
                string isTimeout = res1 == true ? "没超时" : "超时";
                Console.WriteLine(string.Format("结果{0}:{1}", i, isTimeout));
            }
        }
    }
}

运行:

任务前两次是正确的,后面返回的基本全是错误的,原因我估计是 Task 任务内部等待时间是3秒,调用了前两次时间没有超过三秒,所以返回是正确的,后面超过3秒后,全当在超时范围内返回了。

六、其他的写法

超时取消任务的写法可以有多种,其实万变不离其宗,都是用 Task.WhenAny 方法实现的,代码我全部放一个类里面了,有兴趣的可以看看,有很多的高级语法,确实是值得学习的。

代码:

namespace 异步编程1
{
    public static class Lib1
    {
        public static async Task<TResult?> TimeoutAfter<TResult>(this Task<TResult> task, int timeout)
        {
            using (var cancelToken = new CancellationTokenSource())
            {
                Task completedTask = await Task.WhenAny(task, Task.Delay(timeout, cancelToken.Token));
                if (completedTask == task)
                {
                    cancelToken.Cancel();
                    return await task;
                }
                else
                {
                    // 超时处理
                    Console.WriteLine("超时了");
                    return default;
                }
            }
        }
 
        public static async Task<bool> OnTimeout<T>(T t, Action<T> action, int waitms) where T : Task
        {
            if (!(await Task.WhenAny(t, Task.Delay(waitms)) == t))
            {
                action(t);
                return true;
            }
            else
            {
                return false;
            }
        }
 
        public static async Task<bool> TryWithTimeoutAfter<TResult>(this Task<TResult> task, TimeSpan timeout, Action<TResult> successor)
        {
            using var timeoutCancellationTokenSource = new CancellationTokenSource();
            var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token)).ConfigureAwait(continueOnCapturedContext: false);
 
            if (completedTask == task)
            {
                timeoutCancellationTokenSource.Cancel();
 
                // propagate exception rather than AggregateException, if calling task.Result.
                var result = await task.ConfigureAwait(continueOnCapturedContext: false);
                successor(result);
                return true;
            }
            else
                return false;
        }
 
        public static async Task<bool> BeforeTimeout(Task task, int millisecondsTimeout)
        {
            if (task.IsCompleted) return true;
            if (millisecondsTimeout == 0) return false;
            if (millisecondsTimeout == Timeout.Infinite)
            {
                await Task.WhenAll(task);
                return true;
            }
 
            var tcs = new TaskCompletionSource<object>();
 
            using (var timer = new Timer(state => ((TaskCompletionSource<object>)state).TrySetCanceled(), tcs, millisecondsTimeout, Timeout.Infinite))
            {
                return await Task.WhenAny(task, tcs.Task) == task;
            }
        }
    }
}

到此这篇关于C# async/await任务超时处理的实现的文章就介绍到这了,更多相关C# async/await任务超时内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SQLServer批量插入数据的三种方式及性能对比

    SQLServer批量插入数据的三种方式及性能对比

    本文详细讲解了SQLServer批量插入数据的三种方式及性能对比,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-12-12
  • C#获取网页HTML源码实例

    C#获取网页HTML源码实例

    这篇文章主要介绍了C#获取网页HTML源码的方法,是非常实用的技巧,需要的朋友可以参考下
    2014-10-10
  • C#使用WMI获取硬盘参数的实现方法

    C#使用WMI获取硬盘参数的实现方法

    因为需求需要涉及获取硬盘的SN参数,但是又不想要获取到U盘或移动硬盘设备的SN,所以就浅浅的研究了一下,本文给大家介绍了C#使用WMI获取硬盘参数的实现方法,需要的朋友可以参考下
    2024-06-06
  • C#实现操作PPT动画窗格并插入音频文件

    C#实现操作PPT动画窗格并插入音频文件

    这篇文章主要为大家详细介绍了如何利用C#实现操作PPT动画窗格并插入音频文件,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2023-10-10
  • 在C#项目中调用C++编写的动态库的三种方式

    在C#项目中调用C++编写的动态库的三种方式

    这篇文章给大家介绍了三种方式详解如何在C#项目中调用C++编写的动态库,文中通过代码示例给大家介绍的非常详细,具有一定的参考价值,需要的朋友可以参考下
    2024-01-01
  • C#图像伪彩色处理方法

    C#图像伪彩色处理方法

    这篇文章主要介绍了C#图像伪彩色处理方法,涉及C#操作图像的伪彩色相关技巧,需要的朋友可以参考下
    2015-04-04
  • C#实现OFD格式与PDF格式的互转

    C#实现OFD格式与PDF格式的互转

    OFD格式的文档是一种我国独有的国家标准版式的文档。本文将通过C#程序介绍如何实现由OFD与PDF的互相转换,感兴趣的小伙伴可以了解一下
    2022-02-02
  • C#使用ILGenerator动态生成函数的简单代码

    C#使用ILGenerator动态生成函数的简单代码

    这篇文章主要介绍了C#使用ILGenerator动态生成函数的简单代码,需要的朋友可以参考下
    2017-08-08
  • C#实现String类型和json之间的相互转换功能示例

    C#实现String类型和json之间的相互转换功能示例

    这篇文章主要介绍了C#实现String类型和json之间的相互转换功能,涉及C# json格式数据的构造、转换相关操作技巧,需要的朋友可以参考下
    2017-09-09
  • C#编程实现连接ACCESS数据库实例详解

    C#编程实现连接ACCESS数据库实例详解

    这篇文章主要介绍了C#编程实现连接ACCESS数据库的方法,以实例形式较为详细的分析了C#连接access数据库的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-11-11

最新评论