ASP.Net Core异步编程
ASP.Net Core异步编程
概念
什么是异步编程?
异步编程是可以让程序并行运行的一种手段,其可以让程序中的一个工作单元与主应用程序线程分开独立运行,并且在工作单元运行结束后,会通知主应用程序线程它的运行结果或者失败原因。使用异步编程可以提高应用程序的性能和响应能力。[^1]
应当注意的是,所谓的异步编程能提高效率这句话并不严谨,严格的来说它是利用了等待时间以优化整体的时间效率,而对于其中任意一项工作其本来的效率并没提高。
如果你对此概念的理解还是十分抽象,下面我们用一道小学数学题来举例。
小明的妈妈做饭要a分钟,烧水要b分钟,请问小明妈妈烧水并做饭一共要多长时间(a与b均大于0)?我们不妨记最终所用时间为T,则有如下情况:
- 对于传统的同步编程来讲,小明妈妈要先烧水然后做饭或者先做饭然后去烧水,烧水或做饭的时间内是无法做其他事情的,这个等待的过程我们称为阻塞的。这样所用的总时间如下
\]
- 然而对于异步编程,就是让小明妈妈将烧水壶通电进行烧水,烧水由烧水壶负责,而小明妈妈可以一边做饭一边等水烧开。简单来讲就是在等待一件事情完成的同时,利用这段空闲时间去做其他事情,这个过程是非阻塞的。所以所用时间如下:
\]
然而由简单的数学知识可知
\]
所以问题来了:对于烧水或做饭的效率提高了吗?答案是没有。因为烧水仍然要b分钟,做饭仍然要a分钟。然而对于整体的效率却得到了提升,就如上方公式所表示的那样。
async\await关键字
异步方法的定义
在.net中所谓的异步方法,一般是指async关键字修饰的方法。该方法有如下特点:
- 异步方法的返回值一般是
Task<T>,T是真正的返回值类型,如Task<int>。即使方法没有返回值,也最好把返回值声明为非泛型的Task。(按钮等控件事件响应方法用void) - 异步方法名字以Async结尾。
- 调用异步方法时,一般方法前面加上await关键字,这样返回值就是泛型指定的T类型。
- 一个方法中如果有await调用的异步方法,那么该方法也必须是async修饰的异步方法。
下面我们利用C#自带的异步同步方法写入再读取txt文件
同步方法:
//同步
using System;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
string filename = "test.txt"; //读取/写入文件名
File.WriteAllText(filename, "hello,world");
string str = File.ReadAllText(filename);
Console.WriteLine(str);
}
}
}
异步方法:
//异步
using System;
namespace ConsoleApp1
{
class Program
{
static async Task Main(string[] args) //注意这里Main函数变化
{
string filename = "test.txt"; //读取/写入文件名
//如果此处不写await,此处不会等待就进行读取。当数据多的时候,一边写一边读,由于写入操作占用文件,当执行下方读取语句的时候程序会报错。
await File.WriteAllTextAsync(filename, "hello");
/*
* 对于ReadAllTextAsync返回值是Task<string>,添加await后会自动把string从Task拿出来
* 否则要这样写
* Task<string> t = File.ReadAllTextAsync(filename);
* string str = await t;
*/
string str = await File.ReadAllTextAsync(filename);
Console.WriteLine(str);
}
}
}
官方给的方法有了,那么接下来我们写一个自己的异步方法来获取百度的html
using System;
namespace ConsoleApp1
{
class Program
{
static async Task Main(string[] args)
{
int a = await DownloadHtmlAsync("https://www.baidu.com", @"test.txt");
Console.WriteLine("写入完毕!字符串长度为{0}",a);
}
//获取百度htnl并写入文件中,返回html字符串长度
static async Task<int> DownloadHtmlAsync(string url, string filename)
{
HttpClient client = new HttpClient(); //.net5及以上
string html = await client.GetStringAsync(url);
await File.WriteAllTextAsync(filename, html);
return html.Length;
}
}
}
输出结果为
写入完毕!字符串长度为9193
打开生成的可执行文件同一目录下的test.txt发现果然获取到了。Html代码太长,此处我就不放出来了。
那么还会有人问,如果某些地方不支持异步方法,那怎么办呢。其实我们只需要在异步方法后面加.Wait()或.Result就可以了(不推荐),代码如下:
//此处为了简洁仅给出了Main函数部分
static void Main(string args[]) //注意此处的Main我们并没用async关键字
{
File.WriteAllTextAsync("text.txt", "hello,world").Wait();
string str = File.ReadAllTextAsync(@"test.txt").Result;
Console.WriteLine(str);
}
尽管这种方式可以达成目的,但是还是不推荐,因为这种方式可能会面临死锁的风险。
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。[^2]
异步方法的线程委托
在一些情况下我们可能会将异步方法放到线程池来执行。
如果该方法是用正则表达式写的匿名方法的话,则只需要在前面使用async关键字即可。
ThreadPool.QueueUserWorkItem(async (obj) =>{
while(true)
{
await File.WriteAllTextAsync(@"test.txt", "hello,world");
}
});
async\await原理
我们编译如下代码获取百度Html,并向test.txt写入内容做示范
using System;
using System.Net.Http;
namespace ConsoleApp1
{
class Program
{
static async Task Main(string []args) //注意此处的Main我们并没用async限定符
{
using (HttpClient client = new HttpClient())
{
string html = await client.GetStringAsync("https://www.baidu.com");
Console.WriteLine(html);
}
string txt = "hello,world";
string filename = "test.txt";
await File.WriteAllTextAsync(filename, txt);
string str = await File.ReadAllTextAsync(filename);
Console.WriteLine("文件内容: {0}",str);
}
}
}
然后利用ILSpy反编译生成的Dll文件,查看编译器到底给我们做了什么工作来探究async与await背后的原理。
ILSpy可以从GitHub上下载 GitHub - icsharpcode/ILSpy
将ILSpy版本设置成C#4.0

然后发现有两个Main函数

通过查看代码我们便知道,真正的Main实际上是void类型的,这是编译器帮我们搞定的,这个Main中调用了写代码的时候被async修饰返回值为Task的Main函数。
而通过查看<Main>d_0的代码我们可以分析出async与await的底层原理:
- async的方法会被C#编译器编译成一个类,会主要根据await调用切分成多个状态,对async方法的调用会被拆分为MoveNext的调用。
- await看似是等待,实际上编译后没有等待。await调用的等待期间,.net会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续代码。此外这里还进行了优化,到要等待的时候如果发现已经执行结束了,那就没必要切换线程了,剩下的代码继续在之前的线程上执行。
我们为了验证上面await的过程可以去尝试打印线程的ID,只要出现线程ID不同即可证明。代码如下:
//利用异步写入一个很大的此字符串增加时间以防止线程ID相同(字符串较小ID可能会相同)
static async Task Main(string []args)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
StringBuilder sb = new StringBuilder(); //StringBuilder需要using System.Text
for(int i = 0; i < 10000; i++)
{
sb.Append("XXXXXX");
}
await File.WriteAllTextAsync(@"test.txt", sb.ToString());
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
}
在我的PC上运行结果如下,很显然进程ID不一样。
1
10
异步方法不等于多线程
我们来执行下一段代码
using System;
using System.Text;
namespace ConsoleApp1
{
class Program
{
static async Task Main(string []args)
{
Console.WriteLine("之前ID: "+Thread.CurrentThread.ManagedThreadId);
await CalcAsync(5000);
Console.WriteLine("之后: "+Thread.CurrentThread.ManagedThreadId);
}
//n个随机数相加
static async Task<double> CalcAsync(int n)
{
Console.WriteLine("CalcAsync: " + Thread.CurrentThread.ManagedThreadId);
double result = 0;
Random random = new Random();
for(var i = 0; i < n; i++)
result += random.NextDouble();
return result;
}
}
}
查看输出结果中的线程ID发现并没有变。
实际上,异步方法并不会自动在新的线程中执行,除非把代码放到新线程中去。看到这里你可能会问,为什么上文中的线程ID变了呢?
那是因为我们在上文中用当都是内部方法,其实现本就带了Task。
如果我们将上面的CalcAsync方法改成下面的,就能看到进程ID改变了。
static async Task<double> CalcAsync(int n)
{
return await Task.Run(() =>
{
Console.WriteLine("CalcAsync: " + Thread.CurrentThread.ManagedThreadId);
double result = 0;
Random random = new Random();
for (var i = 0; i < n; i++)
result += random.NextDouble();
return result;
});
}
为什么有的异步方法没标async
下面来看这两种方法:
static async Task<string> ReadFileAsync(int num)
{
if (num == 0)
return await File.ReadAllTextAsync("test1.txt");
else if (num == 1)
return await File.ReadAllTextAsync("test2.txt");
else
throw new ArgumentException("num invalid");
}
static Task<string> ReadFileAsync(int num)
{
if (num == 0)
return File.ReadAllTextAsync("test1.txt");
else if (num == 1)
return File.ReadAllTextAsync("test2.txt");
else
throw new ArgumentException("num invalid");
}
首先要说明的是,这两种方法都是符合语法规范的并且结果一致。第一种方法,把Task的string拆出来然后返回时又封装了回去,是一个异步方法;而第二种在调用时相当于直接访问的返回的Task,是一个普通的方法,但使用起来是异步的(相当于直接用File.ReadAllTextAsync)。
然而对于异步方法来讲,通过上文中的反编译可知,异步方法会生成一个类,占用更多的线程,运行效率没有普通方法高。第二种方法很好的避免了这个现象,因此必要时我们可以采用第二种方法。
但如果不是简单地将内部的Task返回出来,而是要对值进行某些操作然后再返回的话(比如将string字符串后面再加上一段),那么只能老实地去使用async与await关键字了。
static async Task<string> ReadFileAsync(int num)
{
if (num == 0)
{
string s = await File.ReadAllTextAsync("test1.txt");
s += "123";
}
else if (num == 1)
{
string s = await File.ReadAllTextAsync("test2.txt");
s += "123";
}
else
throw new ArgumentException("num invalid");
}
异步方法不要使用Sleep
如果想要在异步方法中暂停一段时间,不要用Sleep方法,因为它会阻塞调用线程,降低并发。
Thread.Sleep(3000); // 暂停3000ms,会阻塞线程调用
如果要实现类似效果的话,可以用await关键字加Delay方法 ,用法如下
await Task.Delay(); //异步暂停3000ms
CancellationToken参数
在以前版本的.net给出了Thread.Abort()方法,但这种方法是强制结束线程的,可能产生一些问题,尽量不要去使用它。
有时需要提前终止任务,如请求超时,用户取消请求。很多异步方法都有CancellationToken参数用于提请终止执行的信号。
CancellationToken是一个结构体,它有如下几个成员
None空bool IsCancellationRequested是否取消(*)Register(Action callback)注册取消监听ThrowIfCancellationRequested()如果任务被取消执行到这句话就抛出异常
在创建CancellationToken结构体时,一般不是通过new关键字去完成的,而是通过CancellationTokenSource类去创建的。
CancellationTokenSource有几个重要的方法:
CancelAfter()超时后发出取消信号Cancel()发出取消信号
我们利用上面的知识编写一个方法,要求下载一个网页Html n次,但是在一秒钟后会被取消,当然这个过程是异步的。
static async Task Main(string []args)
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(1000); //1秒后取消操作
CancellationToken cToken = cts.Token;
await DownloadHtml("https://www.baidu.com", 100, cToken);
}
static async Task DownloadHtml(string url, int n, CancellationToken cancellationToken)
{
using(HttpClient client = new HttpClient())
{
for(var i = 0; i < n; i++)
{
string html = await client.GetStringAsync(url);
Console.WriteLine($"{DateTime.Now}:{html}");
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("请求被取消!");
break;
}
}
}
}
接着我们改变下需求,要求一秒钟后抛出异常,代码可以这样写
static async Task DownloadHtml(string url, int n, CancellationToken cancellationToken)
{
using(HttpClient client = new HttpClient())
{
for(var i = 0; i < n; i++)
{
string html = await client.GetStringAsync(url);
Console.WriteLine($"{DateTime.Now}:{html}");
cancellationToken.ThrowIfCancellationRequested();
}
}
}
当然还有一种方式是利用了GetAsync()方法的
static async Task DownloadHtml(string url, int n, CancellationToken cancellationToken)
{
using (HttpClient client = new HttpClient())
{
for (var i = 0; i < n; i++)
{
var resp = await client.GetAsync(url, cancellationToken);
string html = await resp.Content.ReadAsStringAsync();
Console.WriteLine($"{DateTime.Now}:{html}");
}
}
}
那么这两种方式有什么区别呢?如果网站下载特别慢,对于第一种方式要下载完网页才会执行抛出异常的语句,实际上一秒钟后不一定会抛出异常;而第二种方式,则一秒钟后一定会抛出异常。
Task类与WhenAll
Task类的重要方法:
Task<Task> WhenAny(IEnumerable<Task> tasks)等,任何一个Task完成,Task就完成。Task<TResult[]> WhenAll<TResult>(params Task<TResult>[] tasks)等,所有Task完成,Task才完成。用于等待多个任务执行结束但是不在乎它们的执行顺序。FromResult()创建普通数值的Task对象
在这里我们主要来看WhenAll的使用
static async Task Main(string[] args)
{
Task<string> t1 = File.ReadAllTextAsync(@"test1.txt");
Task<string> t2 = File.ReadAllTextAsync(@"test2.txt");
Task<string> t3 = File.ReadAllTextAsync(@"test3.txt");
string[] strs = await Task.WhenAll(t1, t2, t3);
for (var i = 0; i < 3; i++)
Console.WriteLine(strs[i]);
}
异步的其他问题
接口中的异步方法
async是编译器为异步方法中await代码进行分段处理的,而一个异步方法是否修饰了async对于调用者来讲没有区别,因此对于接口中的方法或者抽象方法不能修饰为async。
interface ITest
{
Task<int> GetCharCount(string file); //正确
async Task<int> GetCharCountAsync(string file); //错误,接口中的方法不能用async修饰
}
class Test : ITest
{
public async Task<int> GetCharCount(string file)
{
string s = await File.ReadAllTextAsync(file);
return s.Length;
}
}
异步与yield
yield不仅能简化数据的返回,而且还可以让数据处理“流水线化”提升性能。
关于yield的使用可以参考yield 上下文关键字 - C# 参考 | Microsoft Docs 和 C#中yield用法 - 大西瓜3721 - 博客园
在旧版C#中,async方法中不能用yield。从C#8.0开始,把返回值声明为async IAsyncEnumerable(不要带Task)然后遍历的时候用await foreach()即可。
static async Task Main(string[] args)
{
await foreach (var s in Test()) //注意await
Console.WriteLine(s);
}
static async IAsyncEnumerable<string> Test() //注意没Task
{
yield return "hello";
yield return "world";
}
SynchronizationContext问题
在ASP.Net Core和控制台项目中没有SynchronizationContext因此不用去管ConfigureAwait(false)等。
注意事项
在开发时不要把同步方法和异步方法混用。
结束
异步方法的基本内容大概就这些了,本文章可以看作教程同时也是笔者的学习笔记,感谢 杨中科老师提供的.Net Core课程。
参考
[^1]迷彩风情.认识异步编程[Z].知乎,2020-03-28
ASP.Net Core异步编程的更多相关文章
- 学习ASP.NET Core Razor 编程系列二——添加一个实体
在Razor页面应用程序中添加一个实体 在本篇文章中,学习添加用于管理数据库中的书籍的实体类.通过实体框架(EF Core)使用这些类来处理数据库.EF Core是一个对象关系映射(ORM)框架,它简 ...
- 学习ASP.NET Core Razor 编程系列四——Asp.Net Core Razor列表模板页面
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列五——Asp.Net Core Razor新建模板页面
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列六——数据库初始化
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列七——修改列表页面
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列八——并发处理
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列九——增加查询功能
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列十——添加新字段
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
- 学习ASP.NET Core Razor 编程系列十九——分页
学习ASP.NET Core Razor 编程系列目录 学习ASP.NET Core Razor 编程系列一 学习ASP.NET Core Razor 编程系列二——添加一个实体 学习ASP.NET ...
随机推荐
- JDK1.7HashMap源码分析
1.1首先HashMap中的Hash(哈希)是什么? Hash也称散列,哈希,对应的英文都是Hash.基本原理就是把任意长度的输入通过Hash算法变成固定长度的输出,这个映射的规则就是对应的Ha ...
- Java 多线程共享模型之管程(上)
主线程与守护线程 默认情况下,Java 进程需要等待所有线程都运行结束,才会结束.有一种特殊的线程叫做守护线程,只要其它非守护线程运行结束了,即使守护线程的代码没有执行完,也会强制结束. packag ...
- 深度学习与CV教程(13) | 目标检测 (SSD,YOLO系列)
作者:韩信子@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/37 本文地址:http://www.showmeai.tech/article-det ...
- distroless 镜像介绍及 基于cbl-mariner的.NET distroless 镜像的容器
1.概述 容器改变了我们看待技术基础设施的方式.这是我们运行应用程序方式的一次巨大飞跃.容器编排和云服务一起为我们提供了一种近乎无限规模的无缝扩展能力. 根据定义,容器应该包含 应用程序 及其 运行时 ...
- SAP JSON 格式化及解析。
一.首选:/ui2/cl_json {'key':'value'} /ui2/cl_json=>deserialize( EXPORTING json = json CHANGING d ...
- 拒绝蛮力,高效查看Linux日志文件!
原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处. 简介 日常分析问题时,会频繁地查看分析日志,但如果蛮力去查看日志,耗时费力还不一定有效果,因此我总结了在Linux常用的 ...
- CODING DevOps 助力中化信息打造新一代研效平台,驱动“线上中化”新未来
中化信息技术有限公司,简称"中化信息",是世界 500 强企业中国中化控股有限责任公司(简称"中国中化")的全资直属公司,依托于中国中化的信息化建设实践,建立起 ...
- jfinal中如何使用过滤器监控Druid监听SQL执行?
摘要:最开始我想做的是通过拦截器拦截SQL执行,但是经过测试发现,过滤器至少可以监听每一个SQL的执行与返回结果.因此,将这一次探索过程记录下来. 本文分享自华为云社区<jfinal中使用过滤器 ...
- 实现一个Prometheus exporter
Prometheus 官方和社区提供了非常多的exporter,涵盖数据库.中间件.OS.存储.硬件设备等,具体可查看exporters.exporterhub.io,通过这些 exporter 基本 ...
- Day04 HTML标记
路径 ./ 同级目录 ./ 进入该目录名下 ../ 上一级目录 HTML标记 图片 <!-- 图片标记 src 图片的路径 width 设置图片宽度 height 设置图片高度 title 鼠标 ...