时间:2022-07-15 08:36:05 | 栏目:.NET代码 | 点击:次
刷帖子看到一篇 Go 记录一次groutine通信与context控制 看了一下需求背景,挺有意思的,琢磨了下.net core下的实现
项目中需要定期执行任务A来做一些辅助的工作,A的执行需要在超时时间内完成,如果本次执行超时了,那就不对本次的执行结果进行处理(即放弃这次执行)。同时A又依赖B,C两个子任务的执行结果。B, C之间相互独立,可以并行的执行。但无论B,C哪一个执行失败或超时都会导致本次任务执行失败。
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()