NetCore异步编程CancellationToken
十年河东,十年河西,莫欺少年穷
学无止境,精益求精
关于异步编程,很久之前就写过一遍博客:asp.net core 系列 5 项目实战之:NetCore 的 async 和 await(参考自:Microsoft教程)
今天继续探讨NetCore异步编程,这里会掺杂其他知识点一块讲解
借助之前博客中的一张图,你看到这张图后是否能给出程序的执行顺序?如果你没能回答正确,建议看上述标出的那篇博客。
上图中,五个任务的执行顺序是什么?
这里不做解答,我们从这张图开启本篇
1、异步编程线程切换
同步执行的方法中,方法被执行后,一直到执行结果,整个过程会由一个进程执行。
但异步编程时,当遇到耗时比较长的异步方法时,会引起线程切换,所谓线程切换是指会开启一个新的线程执行耗时较长的异步方法。
先来一个同步执行,如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
static void Main(string[] args) { Console.WriteLine("当前线程ID为:" + Thread.CurrentThread.ManagedThreadId); DownLoadPage_2(); Console.WriteLine("当前线程ID为:" + Thread.CurrentThread.ManagedThreadId); } static void DownLoadPage_2() { for (int i = 0; i < 10; i++) { System.Text.StringBuilder sb = new System.Text.StringBuilder(""); for (int j = 0; j < 100; j++) { sb.Append("我是一只小小小鸟,飞也飞不高。"); File.WriteAllText(@"E:\Test\task_1.txt", sb.ToString()); } } }
执行结果
再来一个异步执行的,如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
static async Task Main(string[] args) { Console.WriteLine("当前线程ID为:" + Thread.CurrentThread.ManagedThreadId); await DownLoadPage(); Console.WriteLine("当前线程ID为:" + Thread.CurrentThread.ManagedThreadId); } static async Task DownLoadPage() { for (int i = 0; i < 10; i++) { System.Text.StringBuilder sb = new System.Text.StringBuilder(""); for (int j = 0; j < 100; j++) { sb.Append("我是一只小小小鸟,飞也飞不高。"); await File.WriteAllTextAsync(@"E:\Test\task_1.txt", sb.ToString()); } } }
综上所示,当执行耗时较长的异步方法后,会出现线程切换的现象,如果异步执行耗时较短,也有可能不切换线程、
因此:
在async 修饰的方法中尽量不要使用 Thread.Sleep(1000); ,应该使用 await Task.Delay(1000);
Thread.Sleep(1000); 方法阻塞的是所有线程,await Task.Delay(1000); 阻塞的是当前线程。
2、CancellationToken的使用
很多异步方法中,都会有CancellationToken参数,如下:
那么,CancellationToken参数有什么作用呢?
2.1、主动取消任务
创建一个 CancellationTokenSource,然后调用异步方法时,传入 CancellationToken,它是一个轻量级对象,可以通知请求是否已取消,我们可以手动调用 cts.Cancel() 来取消任务,为了方面演示,这里我有用到局部方法。
下面的方法,当遇到输入 q 时,方法停止执行
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
static async Task Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); source.CancelAfter(4 * 1000);//运行时间超过4秒,则取消执行 try { await DownLoadPage_3("http://www.baidu.com", 200, source.Token); //输入q 请求被取消 while (Console.ReadLine() == "q") { source.Cancel(); } } catch { Console.WriteLine($"下载超时被取消了"); } } //简单的下载任务 public static async Task DownLoadPage_3(string uri, int num, CancellationToken token) { using (HttpClient client = new HttpClient()) { for (int i = 0; i < num; i++) { var html = await client.GetAsync(uri, token); Console.WriteLine($"第{i + 1}次下载"); //抛出被取消的异常 token.ThrowIfCancellationRequested(); } } }
2.2、定时取消任务
在生产的 CancellationToken中指定请求过期时间,下面的例子为4秒钟过期,4秒钟后,请求终止、
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
static async Task Main(string[] args) { CancellationTokenSource source = new CancellationTokenSource(); source.CancelAfter(4 * 1000);//运行时间超过4秒,则取消执行 try { await DownLoadPage_3("http://www.baidu.com", 200, source.Token); } catch { File.WriteAllText(@"E:\Test\hello.txt", "下载超时被取消了"); Console.WriteLine($"下载超时被取消了"); } } //简单的下载任务 public static async Task DownLoadPage_3(string uri, int num, CancellationToken token) { using (HttpClient client = new HttpClient()) { for (int i = 0; i < num; i++) { var html = await client.GetAsync(uri, token); Console.WriteLine($"第{i + 1}次下载"); //抛出被取消的异常 token.ThrowIfCancellationRequested(); } } }
2.3、HttpClient下载过期时间设定
上述2.2的例子设置为4秒钟后,下载任务终止,因此,我们在使用HttpClinet进行下载操作时,可以设定过期时间,这样做的好处是防止因网络等其他问题导致程序一致处于执行下载过程中,
2.4、在 WebAPI中使用
在webApi中,建议将各个控制器的Action中都注入CancellationToken参数
这样做的好处时,当用户终止操作时,即使程序还在执行,也会随着用户的终止而终止
[HttpGet] public async Task<IActionResult> Get(CancellationToken token = default) { var s = await DownLoadPage(token); return Ok(s); } async Task<string> DownLoadPage(CancellationToken token = default) { var result = string.Empty; using (HttpClient client = new HttpClient()) { for (int i = 0; i < 100; i++) { var s = await client.GetStringAsync("http://www.baidu.com", token); result += s; System.IO.File.WriteAllText(@"E:\Test\hello.txt", $"第{i + 1}次写入"); } return result; } }
上边的例子是一个简单的下载和写入任务,没有实际的意义,下载是为了耗时,写入是为了方便演示效果。
执行结果:
说明随着用户终止操作,方法停止执行。
现在将方法改成如下:
[HttpGet] public async Task<IActionResult> Get(CancellationToken token = default) { var s = await DownLoadPage(CancellationToken.None); return Ok(s); }
不传入CancellationToken,按照上述操作执行方法2秒左右后,将网址定向为百度.com,10秒钟后,看下文本文件写入的次数。
不传入CancellationToken,方法不会随着用户的终止停止执行。
因此:
在webApi中,建议将各个控制器的Action中都注入CancellationToken参数
3、WhenAll 、WhenAny、yield return 的使用
WhenAll 是指所有异步方法都执行完毕后在执行其他
whenAny是指异步方法中有任何一个执行完成后执行其他
yield return 流水化操作,用于提升性能,可用于多任务下载场景使用
本篇博客不做讲解,只贴出代码,如下:
internal class Program { static async Task Main(string[] args) { var task_1 = System.IO.File.ReadAllTextAsync(@"E:\Test\task_1.txt"); var task_2 = System.IO.File.ReadAllTextAsync(@"E:\Test\task_2.txt"); var task_3 = System.IO.File.ReadAllTextAsync(@"E:\Test\task_3.txt"); var result =await Task.WhenAll<string>(task_1, task_2, task_3); foreach(var item in result) { Console.WriteLine(item); } //流水化操作 提升性能 async 中不能使用 yield // yield return 是取到一条数据就返回,适合用于下载场景,例如 yield return "张" 后,会进入Foreach循环 foreach (var item in GetStringArty()) { Console.WriteLine(item); } await foreach (var item in GetStringArty2()) { Console.WriteLine(item); } //List<string> 是填充满后,才会进入foreach foreach (var item in GetStringArty_2()) { Console.WriteLine(item); } } public static IEnumerable<string> GetStringArty() { yield return "张"; yield return "李"; yield return "陈"; } public static async IAsyncEnumerable<string> GetStringArty2() { yield return "张"; yield return "李"; yield return "陈"; } public static IEnumerable<string> GetStringArty_2() { List<string> arys = new List<string>(); arys.Add("张"); arys.Add("李"); arys.Add("陈"); return arys; } }
@天才卧龙的博客