目录
- 一、需求
- 二、Task取消任务
- 三、Task取消任务的回调
- 四、Task超时处理的实现
- 五、Task.WhenAny 的异常
- 六、其他的写法
一、需求
在之前的帖子中,介绍了 async / await 的用法,那么新的问题又来了,如果调用一个异步方法后,一直不给返回值结果怎么办呢?这就涉及到怎么取消任务了。添加一个任务后,如果固定时间内没用返回结果,那么就取消执行,并且在多个任务同时执行的时候,依然按顺序来执行,这就是本文章要实现的功能。
二、Task取消任务
先介绍一下 Task 结束任务的传统用法。在 C# 以前的2.0 等版本中,线程是可以强制终止的,到了后面,就不允许强制去结束线程的,后面微软就提供了一个 CancellationTokenSource 相关的接口开源取消任务。于是我搜索了大量的帖子,一看全是各种抄袭,相互抄来抄去的,搞的我真的火冒三丈,完全是浪费时间,那么总结取消的方法如下:
namespace 取消任务 | |
{ | |
internal class Program | |
{ | |
static CancellationTokenSource source = new CancellationTokenSource(); | |
static void Main(string[] args) | |
{ | |
Task.Run(() => | |
{ | |
for (int i =; i < 10; i++) | |
{ | |
Thread.Sleep(); | |
Console.WriteLine("oh my god"); | |
source.Token.ThrowIfCancellationRequested(); | |
} | |
}, source.Token); | |
Thread.Sleep(); | |
Console.WriteLine("取消任务"); | |
source.Cancel(); | |
Console.ReadKey(); | |
} | |
} | |
} |
运行:
这个可以取消任务是吧,那下面换一个写法:
namespace 取消任务 | |
{ | |
internal class Program | |
{ | |
static CancellationTokenSource source = new CancellationTokenSource(); | |
static void Main(string[] args) | |
{ | |
Task.Run(() => | |
{ | |
Thread.Sleep(); | |
Console.WriteLine("oh my god"); | |
source.Token.ThrowIfCancellationRequested(); | |
}, source.Token); | |
Thread.Sleep(); | |
Console.WriteLine("取消任务"); | |
source.Cancel(); | |
Console.ReadKey(); | |
} | |
} | |
} |
任务中等待三秒,我在等待一秒后取消任务,看看结果:
这回就不管用了,任务明明取消了,但结果依然执行了。
根据他们写的例子,可以总结一点,就是要想取消任务,在任务中,必须加入 for 或者 while 循环,并且在下一轮循环中,执行到 source.Token.ThrowIfCancellationRequested() 这句才能取消任务。
换个写法,如果非得用 for 或者 while 循环这样的语法才能取消任务,我在 while 循环中加入一个判断,如果等于 true,直接跳出循环,这不也是中断了任务,所以说 CancellationTokenSource 真的意义不大
namespace 取消任务 | |
{ | |
internal class Program | |
{ | |
static void Main(string[] args) | |
{ | |
bool isOut = false; | |
var task = Task.Run(() => | |
{ | |
for (int i =; i < 100; i++) | |
{ | |
if (isOut) return; | |
Console.WriteLine("执行中" + i); | |
Thread.Sleep(); | |
} | |
}); | |
Thread.Sleep(); | |
Console.WriteLine("取消任务"); | |
isOut= true; | |
Console.ReadKey(); | |
} | |
} | |
} |
运行:
三、Task取消任务的回调
取消任务也是可以加入回调的,如下:
namespace 取消任务 | |
{ | |
internal class Program | |
{ | |
static CancellationTokenSource source = new CancellationTokenSource(); | |
static void Main(string[] args) | |
{ | |
var task = Task.Run(() => | |
{ | |
for (int i =; i < 100; i++) | |
{ | |
source.Token.ThrowIfCancellationRequested(); | |
Console.WriteLine("执行中" + i); | |
Thread.Sleep(); | |
} | |
}, source.Token); | |
//在指定的毫秒数后取消task执行 | |
source.CancelAfter( * 1000); | |
//取消任务后的回调 | |
source.Token.Register(() => | |
{ | |
//不延迟会获取不到正确的状态 | |
Thread.Sleep(); | |
Console.WriteLine("task状态:" + task1.Status); | |
Console.WriteLine("IsFaulted状态:" + task.IsFaulted);//由于未处理的异常,任务已完成。 | |
Console.WriteLine("IsCompleted状态:" + task.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 配置用来等待 任务的警报,返回值可以获取到改任务 | |
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 Form : Form | |
{ | |
public Form() | |
{ | |
InitializeComponent(); | |
} | |
private void button_Click(object sender, EventArgs e) | |
{ | |
Test(); | |
} | |
private void button_Click(object sender, EventArgs e) | |
{ | |
Test(); | |
} | |
private void Button_ClearConsole_Click(object sender, EventArgs e) | |
{ | |
Console.Clear(); | |
} | |
private async void Test() | |
{ | |
var task = Task.Run(() => | |
{ | |
Thread.Sleep(); | |
return "task"; | |
}); | |
bool res = await task1.TryWithTimeoutAfter(TimeSpan.FromSeconds(4), (string msg) => | |
{ | |
Console.WriteLine("-----------------------task回调:" + msg); | |
}); | |
string isTimeout = res1 == true ? "没超时" : "超时"; | |
Console.WriteLine("任务:" + isTimeout1); | |
var task = Task.Run(() => | |
{ | |
Thread.Sleep(); | |
return "task"; | |
}); | |
bool res = await task2.TryWithTimeoutAfter(TimeSpan.FromSeconds(4), (string msg) => | |
{ | |
Console.WriteLine("-----------------------task回调:" + msg); | |
}); | |
string isTimeout = res2 == true ? "没超时" : "超时"; | |
Console.WriteLine("任务:" + isTimeout2); | |
} | |
private async void Test() | |
{ | |
var task = Task.Run(() => | |
{ | |
Thread.Sleep(); | |
return "task"; | |
}); | |
bool res = await task3.TryWithTimeoutAfter(TimeSpan.FromSeconds(4), (string msg) => | |
{ | |
Console.WriteLine("-----------------------task回调:" + msg); | |
}); | |
string isTimeou = res3 == true ? "没超时" : "超时"; | |
Console.WriteLine("任务:" + 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 异步编程 | |
{ | |
internal class Program | |
{ | |
static void Main(string[] args) | |
{ | |
AwaitReturnValue(); | |
Console.ReadKey(); | |
} | |
public static async void AwaitReturnValue() | |
{ | |
var task = Task.Run(() => | |
{ | |
Thread.Sleep(); | |
return "task"; | |
}); | |
for (int i =; i < 10; i++) | |
{ | |
bool res = await task1.TryWithTimeoutAfter(TimeSpan.FromSeconds(1), (string msg) => | |
{ | |
Console.WriteLine("task回调:" + msg); | |
}); | |
string isTimeout = res == true ? "没超时" : "超时"; | |
Console.WriteLine(string.Format("结果{}:{1}", i, isTimeout)); | |
} | |
} | |
} | |
} |
运行:
任务前两次是正确的,后面返回的基本全是错误的,原因我估计是 Task 任务内部等待时间是3秒,调用了前两次时间没有超过三秒,所以返回是正确的,后面超过3秒后,全当在超时范围内返回了。
六、其他的写法
超时取消任务的写法可以有多种,其实万变不离其宗,都是用 Task.WhenAny 方法实现的,代码我全部放一个类里面了,有兴趣的可以看看,有很多的高级语法,确实是值得学习的。
代码:
namespace 异步编程 | |
{ | |
public static class Lib | |
{ | |
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 ==) 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; | |
} | |
} | |
} | |
} |