.Net5学习笔记(三):异步编程
一、异步方法
异步方法:用Async关键字修饰的方法
- 异步方法的返回值一般是Task
,T是真正的返回值类型,Task , 异步方法名称通常以async结尾 - 即使异步方法没有返回值,也最好把返回值声明成非泛型的Task
- 调用异步方法时 通常在前面叫上await关键字,这样拿到的返回值就是泛型指定的类型
- 异步方法具有"传染性", 一个方法中如果有await调用,则这个方法也要修饰成async方法
public static async Task<int> SumAsync(int a, int b)
{
return await Task.Run(() => a + b);
}
二、异步方法的调用
1. 正常调用,使用await关键字
static async Task Main(string[] args)
{
var a = 10;
var b = 20;
int c = await SumAsync(a, b);
Console.WriteLine(c);
}
- 因为这里使用了await,所以程序运行时会直接当方法返回值Task
中的int类型的值赋给c - Main方法本身为void,但是当内部使用了await时,如果返回值依然用void,运行时就会报错 “程序不包含适合于入口点的静态 "Main" 方法 ”,所以需要将Main方法的返回值设为Task。
2. 不使用await调用
static async Task Main(string[] args)
{
var a = 10;
var b = 20;
Task<int> d = SumAsync(a, b);
Console.WriteLine(d.Result);
}
- 如果不使用await 我们就需要使用Task
类型的值来接受方法返回值,调用Result属性去获取原本的返回值 - await关键字,实际上就是封装了这种获取返回值的方式。
3.其他情况
正常来说,如果使用了await,那么包含该异步方法的方法也要用async修饰。如果说,当前方法不允许使用async修饰,那么就是用以下方式解决
3.1. 有返回值 调用Result方法
static async Task Main(string[] args)
{
var a = 10;
var b = 20;
// 1. 有返回值 调用Result方法
c = SumAsync(a, b).Result;
Console.WriteLine(c);
}
3.2. 没有返回值,调用Wait方法
首先添加一下简单的没有返回值的方法
public static async Task ConsoleString(string str)
{
await Task.Run(() => Console.WriteLine(str));
}
下面是调用方式
static async Task Main(string[] args)
{
ConsoleString("sdad").Wait();
}
需要注意的是使用这两种方式都有死锁的风险
三、 异步方法的原理
将编译器生成的dll放到反编译器中,可以得出以下结论
详细查看杨中科老师的视频
await、async是语法糖,最终编译成状态机调用,async方法会被C#编译器编译成一个类,会根据await调用进行切分成多个状态,对async方法的调用会被拆分为对多个MoveNext的调用,用await看似是“等待”,经过编译后,其实并没有wait
四、异步编程与多线程之间的管理
- await调用的等待期间,.Net会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出来一个线程执行后续的代码。到要等待的时候,如果异步方法已经执行完毕,那么就继续在当前线程执行,没有必要在切换线程。验证代码:
public static async Task RelationShip1()
{
try
{
Console.WriteLine($"当前线程id:{Thread.CurrentThread.ManagedThreadId}");
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
stringBuilder.Append("sssssssssssssssssssssssssssssss");
}
await File.WriteAllTextAsync("RelationShip.txt", stringBuilder.ToString());
Console.WriteLine($"当前线程id:{Thread.CurrentThread.ManagedThreadId}");
}
catch (Exception)
{
throw;
}
}
方法运行后的输出结果:
当前线程id:1
当前线程id:4
- 异步方法并不会自动到一个新的线程中执行,除非手动切换,上个方法出现了线程切换是因为File.WriteAllTextAsync内部存在着线程操作(猜测)。测试代码:
public static async Task RelationShip2()
{
Console.WriteLine($"运行前:{Thread.CurrentThread.ManagedThreadId}");
var result = await CalcAsync(5000);
Console.WriteLine($"Result: {result}");
Console.WriteLine($"运行后:{Thread.CurrentThread.ManagedThreadId}");
}
public static async Task<decimal> CalcAsync(int n)
{
Console.WriteLine($"当前线程id:{Thread.CurrentThread.ManagedThreadId}");
decimal result = 0;
Random random = new Random();
for (int i = 0; i < n; i++)
{
result += (decimal)random.NextDouble();
}
return result;
}
方法运行后的输出结果:
运行前:9
当前线程id:9
Result: 2520.063400485160281679
运行后:9
五、没有async方法标识的异步方法
public static Task<string> ReadAsync(int num)
{
switch (num)
{
case 0:
return File.ReadAllTextAsync("RelationShip.txt");
case 1:
return File.ReadAllTextAsync("text.txt");
default:
throw new ArgumentOutOfRangeException(nameof(num));
}
}
上面的方法内部虽然进行了异步操作,但是方法并没有使用async修饰,这是因为这个方法只是对File.ReadAllTextAsync的调用的转发,不包含其他的逻辑,这个时候是可以不适用async关键字修饰的
- 使用async修饰方法的缺点
- 异步方法会生成一个类,运行效率没有普通方法高
- 可能会占用更多的线程
- 没有使用async修饰的方法的优点
只返回Task
与普通方法相同,运行效率高,不会造成线程的浪费
六、CancellationToken
CancellationToken: 结构体,用来传递异步方法提前结束的信息
IsCancellationRequested:获取是否已请求取消此标记。
ThrowIfCancellationRequested(): 如果已请求取消此标记,则引发 OperationCanceledException。
None:返回一个空 CancellationToken 值。
6.1. 使用实例1 获取网页内容n次,5秒后超时停止
public static async Task Test1()
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await ReadHtml(10000, cts.Token);
}
public static async Task ReadHtml(int n, CancellationToken token)
{
using (HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
string html = await client.GetStringAsync("https://www.bilibili.com/");
Console.WriteLine(html);
if (token.IsCancellationRequested)
{
Console.WriteLine("超时停止");
break;
}
}
}
}
CancellationToken一般不直接创建对象,而是使用CancellationTokenSource
6.2. 使用实例2 获取网页内容n次,用户输入回车后停止
public static async Task Test1()
{
CancellationTokenSource cts = new CancellationTokenSource();
// 此处如果添加await的话 程序不会往下执行
_ = ReadHtml(10000, cts.Token);
while (Console.ReadKey().Key == ConsoleKey.Enter)
{
// 手动停止
cts.Cancel();
}
}
public static async Task ReadHtml(int n, CancellationToken token)
{
using (HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
string html = await client.GetStringAsync("https://www.bilibili.com/");
Console.WriteLine(html);
if (token.IsCancellationRequested)
{
Console.WriteLine("超时停止");
break;
}
}
}
}
6.3. 使用实例3 有调用的异步方法自行处理异步停止逻辑
public static async Task Test1()
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await ReadHtml(10000, cts.Token);
}
public static async Task ReadHtml(int n, CancellationToken token)
{
using (HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
string html = await client.GetStringAsync("https://www.bilibili.com/", token);
Console.WriteLine(html);
}
}
}
超时5秒后,获取网页的一步逻辑自行停止
使用这种方式处理一步停止逻辑的话 可以更加及时的停止异步逻辑
6.4. 在Asp.Net项目中使用
在Asp.Net项目中使用时只需要在action方法中添加CancellationToken token参数,然后自行处理停止逻辑,.Net会自动注入数据,并且在用户停止操作后调用CancellationToken。

浙公网安备 33010602011771号