2.1 为什么要用异步编程
原著中用一个餐馆点餐的例子介绍了异步编程的实况:餐馆点餐时的常规情况是在只有一桌客人的情况下服务员在旁边等待记录顾客所点的菜单,知道顾客点完菜,才完成一个完成的点餐任务。而实际情况是在用餐时间段并不是只有一桌客人而是多批客人同时到店内就餐,此时如果店内服务员较少的话,其他客人就得等待正在点餐的那波顾客完成点餐任务后才能开始点餐,这个时候就需要“异步点餐”来应对这个问题了,即服务员在顾客点餐时把菜单及记录单和笔交给顾客,让顾客【选择菜品自行记录】不用等待顾客点餐,这个【选择菜品自行记录】就是异步点餐的核心内容,也对应了后面所学的异步方法,当有新的顾客进店后执行同样的操作,这样就实现了一个服务员应多多批顾客同时点餐的方法,整体提升了店内顾客点餐的效率。
2.2 轻松上手await async
这一节主要讲了await async的用法及异步方法的注意事项,注意事项主要有以下几点:
- (1)异步方法的返回值一般是 Task<T>泛型类型,其中的T是真正的返回值类型,比如方法想要返回 int 类型,返回值就要写成 Task<int>。Task 类型定义在 System.Threading.Tasks 命名空间下。
例如系统IO中的File类读写文件的方法,返回类型为string
//
// 摘要:
// Asynchronously opens a text file, reads all the text in the file, and then closes
// the file.
//
// 参数:
// path:
// The file to open for reading.
//
// cancellationToken:
// The token to monitor for cancellation requests. The default value is System.Threading.CancellationToken.None.
//
//
// 返回结果:
// A task that represents the asynchronous read operation, which wraps the string
// containing all text in the file.
public static Task<string> ReadAllTextAsync(string path, CancellationToken cancellationToken = default);
- (2) 按照约定,异步方法的名字以 Async结尾,虽然这不是语法的强制要求,但是方法以Async 结尾可以让开发人员一眼就看出来它是异步方法。实例如(1)所示
- (3)如果异步方法没有返回值,可以把返回值声明为void,这在语法上是成立的。但这样的代码在使用的时候有很多的问题,而且很多框架都要求异步方法的返回值不能为void,因此即使方法没有返回值,也最好把返回值声明为非泛型的 Task 类型。
- (4)调用泛型方法的时候,一般在方法前加上await 关键字,这样方法调用的返回值就是泛型指定的T类型的值。
- (5)一个方法中如果有await 调用,这个方法也必须修饰为 async,因此可以说异步方法是有传染性的。
2.2.1 await async关键字介绍
await关键字的作用:通常用于调用异步方法或耗时调用一个耗时方法,写在调用方法的前面,等待异步方法执行结束后再继续向下执行。
async关键字的作用:修饰方法,使其成为异步方法。
注意:在使用await调用方法时,该方法是否被修饰为async不是必要条件,修饰async只是为了在方法内使用await关键字。
而调用被async修饰的方法是否使用await关键字也不是必要条件,根据实际情况(例如该耗时操作与后续代码的执行没有关联)如果不需要等待该异步方法执行结束则不用await关键字,但VS会提示建议使用await,如下图所示
2.2.1 await async关键字使用代码实例
await使用:以调用系统IO的文件流File类为例
Console.WriteLine("befor write file!");
await File.WriteAllTextAsync("D:/blabla.txt","我在草台子上学.NET!");
Console.WriteLine("befor read file!");
string readTxtString= await File.ReadAllTextAsync("D:/blabla.txt");
Console.WriteLine(readTxtString);
执行结果
async使用:修饰方法
public class AsyncFunctionClass
{
public static async Task WriteTxtAsync()
{
string readTxtString = await File.ReadAllTextAsync("D:/blabla.txt");
Console.WriteLine(readTxtString);
Console.ReadKey();
}
}
在关键字注意事项中提到了修饰async只是为了在方法内使用await关键字,这就意味着方法中如果使用了await,则该方法必须用async修饰,否则VS会报错提示
2.3 怎么编写异步方法
其实上文中以及提到过异步方法的编写并做出了示例,原著中用一个http请求示例演示了异步方法的编写,并做出了相信的注释,这里用fittencode助手简单注释一下
// 下载人民邮电出版社有限公司网站首页并保存到本地文件的异步方法
Console.WriteLine("开始下载人民邮电出版社有限公司网站首页");
int il = await DownloadAsync("https://www.ptpress.com.cn", "d:/ptpress.html");
Console.WriteLine($"下载完成,长度(11)");
// 异步下载指定URL的内容并保存到本地文件,返回文件内容长度
async Task<int> DownloadAsync(string url, string destFilePath)
{
// 使用HttpClient下载指定URL的内容
using HttpClient httpClient = new HttpClient();
string body = await httpClient.GetStringAsync(url);
// 将下载的内容写入到本地文件
await File.WriteAllTextAsync(destFilePath, body);
return body.Length;
}
执行结果
2.4 async await 原理揭秘
本节通过对异步方法的项目生成的dll文件进行反编译,剖析出了异步方法调用及执行的底层原理及其过程,让读者对异步编程从知其然到知其所以然有了更深刻的认识。编译过程用了dnSpy反编译器(原著用了dnSpy,视频教程用的是ILSpy,随意,ILSpy更全面一些)。这里我用了ILSpy,ILSpy下载地址如下GitHub - icsharpcode/ILSpy: .NET Decompiler with support for PDB generation, ReadyToRun, Metadata (&more) - cross-platform!直接下载项目ZIP启动解决方案即可使用。
首先编写项目源码并进行编译
using static System.Net.Http.HttpClient;
class Program
{
static async Task Main(string[] args)
{
using (HttpClient httpClient = new HttpClient())
{
string html = await httpClient.GetStringAsync("https://www.baidu.com");
Console.WriteLine(html);
}
string destFilePath = "d:/1234.txt";
string content = "hello async andawait";
await File.WriteAllTextAsync(destFilePath, content);
string content2 = await File.ReadAllTextAsync(destFilePath);
Console.WriteLine(content2);
}
}
编译后的目录结果为
用反编译工具进行反编译(项目名称.dll文件),据老杨所讲,切换到较低的语言版本会反编译的更加详细一些,反编译后的目录为
反编译后类<Main>d_0的代码
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Runtime.CompilerServices;
[CompilerGenerated]
private sealed class <Main>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
public string[] args;
private string <destFilePath>5__1;
private string <content>5__2;
private string <content2>5__3;
private HttpClient <httpClient>5__4;
private string <html>5__5;
private string <>s__6;
private string <>s__7;
private TaskAwaiter<string> <>u__1;
private TaskAwaiter <>u__2;
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter awaiter2;
TaskAwaiter<string> awaiter;
switch (num)
{
default:
<httpClient>5__4 = new HttpClient();
goto case 0;
case 0:
try
{
TaskAwaiter<string> awaiter3;
if (num != 0)
{
awaiter3 = <httpClient>5__4.GetStringAsync("https://www.baidu.com").GetAwaiter();
if (!awaiter3.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter3;
<Main>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter3, ref stateMachine);
return;
}
}
else
{
awaiter3 = <>u__1;
<>u__1 = default(TaskAwaiter<string>);
num = (<>1__state = -1);
}
<>s__6 = awaiter3.GetResult();
<html>5__5 = <>s__6;
<>s__6 = null;
Console.WriteLine(<html>5__5);
<html>5__5 = null;
}
finally
{
if (num < 0 && <httpClient>5__4 != null)
{
((IDisposable)<httpClient>5__4).Dispose();
}
}
<httpClient>5__4 = null;
<destFilePath>5__1 = "d:/1234.txt";
<content>5__2 = "hello async andawait";
awaiter2 = File.WriteAllTextAsync(<destFilePath>5__1, <content>5__2).GetAwaiter();
if (!awaiter2.IsCompleted)
{
num = (<>1__state = 1);
<>u__2 = awaiter2;
<Main>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter2, ref stateMachine);
return;
}
goto IL_0177;
case 1:
awaiter2 = <>u__2;
<>u__2 = default(TaskAwaiter);
num = (<>1__state = -1);
goto IL_0177;
case 2:
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter<string>);
num = (<>1__state = -1);
break;
}
IL_0177:
awaiter2.GetResult();
awaiter = File.ReadAllTextAsync(<destFilePath>5__1).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 2);
<>u__1 = awaiter;
<Main>d__0 stateMachine = this;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
break;
}
<>s__7 = awaiter.GetResult();
<content2>5__3 = <>s__7;
<>s__7 = null;
Console.WriteLine(<content2>5__3);
Console.ReadKey();
}
catch (Exception exception)
{
<>1__state = -2;
<destFilePath>5__1 = null;
<content>5__2 = null;
<content2>5__3 = null;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<destFilePath>5__1 = null;
<content>5__2 = null;
<content2>5__3 = null;
<>t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
从目录发现,反编译后不仅生成了Main函数的反编译结果,还生成了一个名为<Main>d_0的类,该类实现了一个叫IAsyncStateMachine(状态机)的接口。反编译后的代码较为复杂,并且变量名等内容进行了重命名,不用详细研究,反编译的主旨是探究异步方法底层的调用原理:编译器会把程序分割成多个片段,每个片段对应一个状态,并且把这些片段分别放入MoveNext方法中的Switch语句里面,IAsyncStateMachine中用MoveNext方法实现了状态机模式,用<>1__state变量记录了当前执行到哪个状态,MoveNext在程序执行过程中被多次调用,每次被调用代表进入下一个状态。
看视频教程和书会有更全面的理解:
2.5 async背后的线程切换
2.5.1 线程切换代码演示
线程切换:在对异步方法进行 await 调用的等待期间,框架会把当前的线程返回给线程池,等异步方法调用执行完毕后,框架会从线程池再取出一个线程,以执行后续的代码。
编写异步方法
public static async Task ThreadSwitch()
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine("异步执行前的线程ID:"+Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("执行异步");
await File.WriteAllTextAsync("D:/8.txt","我在巴黎零元购");
Console.WriteLine("异步执行后的线程ID:" + Thread.CurrentThread.ManagedThreadId);
await File.WriteAllTextAsync("D:/7.txt", "我在巴黎零元购");
Console.WriteLine("再次异步执行后的线程ID:" + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("执行同步");
File.WriteAllText("D:/9.txt", "我在巴黎零元购");
Console.WriteLine("同步执行后的线程ID:" + Thread.CurrentThread.ManagedThreadId);
}
调用并查看执行结果
从执行结果来看,在执行异步调用的前后,程序并不在同一线程执行,也证明了在异步等待期间线程会被放回线程池,异步执行结束后再从线程池中获取一个空闲线程用来执行后续代码。这种模式的好处是计算机中的每个线程都不会空等某个耗时操作,很好的提升了计算机处理并发请求的能力。
结合async的执行逻辑,编译器把 async 拆分成多次方法调用,程序在运行的时候会通过从线程池中取出空闲线程执行不同 MoveNext 调用的方式来避免线程的“空等”,这样开发人员就可以像编写同步代码一样编写异步代码,从而提升系统的并发处理能力。
2.6 异步方法不等于多线程
异步方法不等于多线程,异步方法中的代码不会自动在新的线程中执行,除非手动把代码放到新的线程中执行。
下面用一段代码验证
static async Task Main(string[] args)
{
Console.WriteLine("Main的线程ID:" + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(await TestThread());
Console.WriteLine("Main的线程ID:" + Thread.CurrentThread.ManagedThreadId);
}
static async Task<int> TestThread()
{
int reInt = 1;
Console.WriteLine("异步方法中的线程ID:" + Thread.CurrentThread.ManagedThreadId);
for (int i = 1; i < 100000000; i++)
{
reInt += i;
}
Console.WriteLine("异步方法中的线程ID:" + Thread.CurrentThread.ManagedThreadId);
return reInt;
}
执行结果
如何让代码在新的线程中执行?Task.Run
static async Task Main(string[] args)
{
Console.WriteLine("Main的线程ID:" + Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(await TestThread());
Console.WriteLine("Main的线程ID:" + Thread.CurrentThread.ManagedThreadId);
}
static async Task<int> TestThread()
{
int reInt = 1;
Console.WriteLine("异步方法中的线程ID:" + Thread.CurrentThread.ManagedThreadId);
await Task.Run(() =>
{
Console.WriteLine("Task.Run的线程ID:" + Thread.CurrentThread.ManagedThreadId);
for (int i = 1; i < 100000000; i++)
{
reInt += i;
}
});
Console.WriteLine("异步方法中的线程ID:" + Thread.CurrentThread.ManagedThreadId);
return reInt;
}
执行结果
个人理解,前文中提到的线程切换其实就是将一个任务放到新的线程中进行执行了。
2.7 异步编程的重要问题
(1)建议尽可能使用.NET Core类库时使用异步方法,提升并发处理能力。
(2)建议使用await调用异步方法,不建议在返回值对象上调用Result属性或GetAwaiter().GerResult或Wait()
static async Task Main(string[] args)
{
string s = File.ReadAllTextAsync("D:/8.txt").Result;
string s1 = File.ReadAllTextAsync("D:/8.txt").GetAwaiter().GetResult();
File.WriteAllTextAsync("D:/8.txt", "我在巴黎零元购").Wait();
}
因为这样会阻塞线程,使并发处理能力下降,甚至程序死锁。
(3)异步暂停的方法:不要用Thread.Sleep,他会阻塞调用线程,应该使用await Task.Delay。
(4).NET Core的很多异步方法都有CancellationToken类型的参数,可以在需要时通过CancellationToken对象终止某个异步操作。
(5)等待所有线程执行完 await Task.WhenAll(线程1,线程2),等待任意线程完成await Task.WhenAny(线程1,线程2)。
(6)接口中的方法或抽象方法不可以用async修饰,但可以把这些方法的返回值设置为Task类型,在实现类中可以根据需求进行async修饰。