一、引言
C#中经常可以看到在一个方法前面有async修饰符。比如,最近在学习Quartz.NET,在教程中就有这样的写法:
private static async Task Main(string[] args)
{
// Grab the Scheduler instance from the Factory
StdSchedulerFactory factory = new StdSchedulerFactory();
IScheduler scheduler = await factory.GetScheduler();
// and start it off
await scheduler.Start();
// some sleep to show what's happening
await Task.Delay(TimeSpan.FromSeconds(10));
// and last shut down the scheduler when you are ready to close your program
await scheduler.Shutdown();
}
并且在async修饰的方法中,总会出现await的身影。所以你想抛开async和await中的某一个,去单独认识另一个是很难的。
今天,花一篇博客的功夫来简单认识一下这两者。
二、async与await
1. async概述
首先这里的async是一个关键字,同时也是修饰符(和abstract、static一样)。
使用async修饰符可以将一个方法、lambda表达式或匿名方法指定为异步的。如果在方法或表达式上使用async修饰符,则它就被称为异步方法(async method)。下面示例代码定义了一个名为ExampleMethodAsync的异步方法:
public async Task<int> ExampleMethodAsync()
{
//...
}
官方文档原文中,接下来有这么一段话:
如果你是一个异步编程的新手,或不知道异步方法如何使用await操作符来执行可能长时间运行的工作,而不阻塞调用者的线程,请阅读使用async和await异步编程的介绍。
显然,此时牵涉到了await,不过上面这篇文章太长,这边就不展开讲了,直接看await的简单描述。
2. await概述
await是一个operator(运算符或者操作符),该运算符会挂起(suspend)封闭的异步方法(async method),直到操作对象的异步操作完成。
这句话初看比较难懂,稍微拆解一下。
- operator,运算符,跟加减乘除一样,作用于某个值(或对象),然后该值会进行一些运算,发生变化。
- 挂起,就是使某个过程暂停。
- 封闭的,我们可以想象方法(或者说函数)是一个容器,里面装载了一些运算的语句,随着运算的进行,方法(容器)中的状态会发生变化,此时我挂起方法,就相当于把方法(连同那些状态)封闭起来,不再改变。
- 异步方法,指的是该方法不是阻塞的,我运行到某个点,可能要等很久,此时我不等了,直接去干别的事情了,该点运行完之后通知我回来继续运行。
- 直到操作对象的异步操作完成,就是说await作用的对象的其他异步操作还在进行,进行完了我再回来继续执行await下面的语句。
当异步操作完成时,await运算符返回运算的结果(如果有的话)。
当await运算符应用于已完成运算的操作数(没有其他异步运算了)时,它会立即返回运算结果,而不会暂停封闭方法。await运算符不会阻塞运算异步方法的线程。当await运算符挂起封闭的异步方法时,控制权会返回到方法的调用方。
下面画了个草图,因为用的代码绘图,有的地方有歧义,不是绝对准确的,但大致就是这个意思。
- 这里的OperationAsync是个多线程(不一定要多线程,只要是异步操作就行。不过用代码来演示的话,你想表现出await的效果,多开一个线程比较方便一些。因为去操控IO要写许多其他代码)的异步操作。
- 蓝色手描实线表示该方法正在执行。
异步≠多线程,但异步往往会和多线程一起用。
3. await代码示例
下面是官方代码示例,
HttpClient.GetByteArrayAsync方法返回Task<byte[]>实例,它表示一个异步操作,该操作完成时会生成一个字节数组(byte[])。
在操作完成之前,await操作符会挂起DownloadDocsMainPageAsync方法。
当DownloadDocsMainPageAsync挂起时,控制权会返回给Main方法,Main方法是DownloadDocsMainPageAsync的调用者。
Main方法一直执行,直到它需要DownloadDocsMainPageAsync方法执行的异步操作的结果。
当GetByteArrayAsync获得所有字节时,DownloadDocsMainPageAsync方法的其余部分继续执行。在这之后,Main方法的剩余部分继续执行。
using System;
using System.Net.Http;
using System.Threading.Tasks;
public class AwaitOperator
{
public static async Task Main()
{
Task<int> downloading = DownloadDocsMainPageAsync();
Console.WriteLine($"{nameof(Main)}: Launched downloading.");
int bytesLoaded = await downloading;
Console.WriteLine($"{nameof(Main)}: Downloaded {bytesLoaded} bytes.");
}
private static async Task<int> DownloadDocsMainPageAsync()
{
Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: About to start downloading.");
var client = new HttpClient();
byte[] content = await client.GetByteArrayAsync("https://docs.microsoft.com/en-us/");
Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: Finished downloading.");
return content.Length;
}
}
// Output similar to:
// DownloadDocsMainPageAsync: About to start downloading.
// Main: Launched downloading.
// DownloadDocsMainPageAsync: Finished downloading.
// Main: Downloaded 27700 bytes.
只能在async关键字修饰的方法、lambda表达式或匿名方法上使用await运算符。在异步方法中,你不能在同步函数体中、锁的语句块中和不安全的上下文中使用await运算符。
await运算符的操作对象通常是以下几种.NET类型之一:
- Task
- Task<TResult>
- ValueTask
- ValueTask<TResult>
不过,任何可等待(awaitable)的表达式都可以是await运算符的作用对象。
如果表达式t的类型是Task<TResult>或Value<TResult>,则await t的类型是TResult。
如果t的类型是Task或ValueTask,则await t的类型是void。
这两种情况下,若t抛出异常,则await t重新抛出异常。
简单讲就是,表达式带参数返回参数类型,不带参数返回空,抛出异常就抛出异常。
小结
那么在两个小节的简单介绍之后,用一句话来总结一下await。
await运算符,会等待操作对象的异步操作完成,并会挂起函数,让出执行权。
4. async代码示例
在对await有个大概理解后,继续学习async关键字。
下面代码是在一个异步方法中的,并且它调用了 HttpClient.GetStringAsync方法:
string contents = await httpClient.GetStringAsync(requestUrl);
异步方法会以同步的方式运行,直到遇到await表达式,此时该方法被挂起,直到等待的任务完成。与此同时,控制(执行)权返回给方法的调用者。
这段描述和await中描述的一样。
如果async关键字修饰的方法不包含await表达式或语句,则该方法将同步执行。编译器会警告你该异步方法不包含await语句,因为这种情况可能会指示错误。详情看编译器警告CS4014。
async关键字是上下文相关的,因为它只有在修饰方法、lambda表达式或匿名方法时才是关键字。在其它场合下,它被解释成标识符。
下面正式看一个例子,该示例展示了异步事件处理器StartButton_Click和异步方法ExampleMethodAsync之间的控制结构与流程。该异步方法的执行结果是网页的字符数。该代码适用于WPF程序。
在WPF中运行它,你需要一个名为StartButton的按钮控件和一个名为ResultsTextBox的文本框控件。不要忘了给它们设置名称(name)和处理器(handler),就像下面这样:
<Button Content="Button" HorizontalAlignment="Left" Margin="88,77,0,0" VerticalAlignment="Top" Width="75"
Click="StartButton_Click" Name="StartButton"/>
<TextBox HorizontalAlignment="Left" Height="137" Margin="88,140,0,0" TextWrapping="Wrap"
Text="<Enter a URL>" VerticalAlignment="Top" Width="310" Name="ResultsTextBox"/>
作为WPF程序运行:
- 将代码粘贴到MainWindow.xaml.cs中的MainWindow类中。
- 添加System.Net.Http引用。
- 为System.Net.Http添加using指令。
private async void StartButton_Click(object sender, RoutedEventArgs e)
{
// ExampleMethodAsync returns a Task<int>, which means that the method
// eventually produces an int result. However, ExampleMethodAsync returns
// the Task<int> value as soon as it reaches an await.
ResultsTextBox.Text += "\n";
try
{
int length = await ExampleMethodAsync();
// Note that you could put "await ExampleMethodAsync()" in the next line where
// "length" is, but due to when '+=' fetches the value of ResultsTextBox, you
// would not see the global side effect of ExampleMethodAsync setting the text.
ResultsTextBox.Text += String.Format("Length: {0:N0}\n", length);
}
catch (Exception)
{
// Process the exception if one occurs.
}
}
public async Task<int> ExampleMethodAsync()
{
var httpClient = new HttpClient();
int exampleInt = (await httpClient.GetStringAsync("http://msdn.microsoft.com")).Length;
ResultsTextBox.Text += "Preparing to finish ExampleMethodAsync.\n";
// After the following return statement, any method that's awaiting
// ExampleMethodAsync (in this case, StartButton_Click) can get the
// integer result.
return exampleInt;
}
// The example displays the following output:
// Preparing to finish ExampleMethodAsync.
// Length: 53292
三、结尾
说实话,通过这篇文章想对这对组合的应用场景有较深认识是做不到的。
在学习这对关键字前,最好对同步异步有个认识。
这里的异步操作放应用程序编程里,我觉得大部分时候都是一个多线程的任务,除非你会去操作一些IO。
-
归纳
- 1️⃣async和await往往一起出现
- 2️⃣async修饰符,指明该方法是一个异步方法
- 3️⃣await运算符,等候操作对象的异步操作完成
目前来看,这对组合用于控制执行顺序非常有用,比如:
// 假设Fun2是一个很耗时的方法
async Task Fun1 ()
{
await Task.Run(()=>{
Fun2();
});
Fun3();
}
如果没有await和Task.Run,那运行至Fun2,可能就卡很久,再运行Fun3。
如果没有await,只有Task.Run,那Fun2和Fun3执行顺序不一定。
如果有await和Task.Run,则运行至Fun2,执行权转移,等Fun2执行完再执行Fun3。