Async and Await 异步和等待

时间:2021-11-06 23:13:08

【第一次这么耐下性子认真写博客,虽然觉得很认真了,当毕竟是第一次嘛,以后再看肯定觉得很不咋滴的,更何况园子里有那么多的高人和大侠,这篇文章就权当练练手了,熟悉一下用客户端发表博客了,也希望大家多多照顾新人,这厢有礼了Async and Await 异步和等待!】下面正式开始,GO!


目录

Introducing the Keywords 介绍关键字
Awaitables 异步操作
Return Types 返回类型
Returning Values 返回值
Context  上下文
Avoiding Context:避免上下文
Async Composition 异步组合
Guidelines 指南
Next Steps 下一步

首先,号外号外:异步将从根本上改变大多数编写代码的方式。是的,我相信异步/等待将会比Linq的影响更大。理解异步将会是未来几年的基本必须品。

Introducing the Keywords 介绍关键字

最简单的异步方法的样子就像下面这样:

public async TaskDoSomethingAsync()
{
awaitTask.Delay(100);//异步等待100ms
}

async关键字在这个方法中启用了await关键字,并且改变了方法处理结果的方式。这就是async关键字所做的!它没有在线程池的线程上运行这个方法,也没有做任何其他奇妙的事情。async关键字仅仅启用了await关键字(并且管理方法结果)。

异步方法从一开始就像其他任何方法一样执行下去,即,一开始同步运行直到遇到一个”await“(或者抛出一个异常)。

await关键字是可以获得异步的地方。await就像一个一元操作符:它只需要一个参数,一个awaitable(一个"awaitable"是一个异步操作)。await检查异步操作是否已经完成;如果异步操作已经完成了,那么这个方法就继续执行(就像一个普通的同步方法一样)。

如果await看到了异步操作没有完成,那么它就异步执行。它告诉这个异步操作当此异步操作本身完成的时候,运行这个方法的剩余部分,然后返回到该异步方法。

然后,当异步操作完成时,将执行异步方法的剩余部分。如果你正在等待一个内置的异步操作(比如一个任务),然后异步方法的剩余部分将会在”await“返回之前被捕捉到的”上下文“上执行。

我喜欢把”await“看做是”异步的await“。那就是说,在异步操作完成之前,异步方法一直是暂停的(因此它等待),但是实际的线程并没有阻塞(所以它是异步的)。

Awaitables 异步操作

.NET framework中有2种已经共用的异步操作类型: Task<T> 和Task。也有其他的异步操作类型:特殊的方法,如“Task.Yield”返回不是任务类型的异步操作。你也可以创建自己的异步操作(经常出于性能原因考虑),或者使用扩展方法来产生非异步操作类型的异步操作。

关于异步操作的很重要的一点是:这个类型是异步操作,此类型不是指方法返回的类型。换言之,你可以等待返回Task的异步方法的结果...因为这个方法返回Task,而不是因为它是异步的。所以你可以等待返回Task的非异步的方法的结果,如下:

public async Task NewStuffAsync()
{
// 使用await.
await ...
} public Task MyOldTaskParallelLibraryCode()
{
// 注意该方法不是一个异步方法,不能在该方法体内使用await
...
} public async Task ComposeAsync()
{
// 我们可以 await Tasks,不管它们来自哪里.
await NewStuffAsync();
await MyOldTaskParallelLibraryCode();
}

提示:如果你有一个简单的异步的方法,你可以不使用await关键字来实现它(如使用Task.FromResult)。如果你可以不使用await,那就不要使用,并且移除async关键字。一个返回Task.FromResult的非异步方法比返回一个值的异步方法更高效。

Return Types 返回类型

异步方法可以返回Task,Task<T>或者void。在几乎所有情况下,有可以返回Task或Task<T>,只有必要时才返回void。

为啥返回Task或者Task<T>呢?因为他们是异步操作,而void不是。所以如果你有一个返回Task或者Task<T>的方法,那么你可以把结果传递给await。对于返回值为void的方法,你没有东西传给await。当你有同步事件句柄的时候,必须返回void。

对于其他一些高级的操作你可以使用async void,比如一个单独的“static async void MainAsync()”控制台程序。然而,async void的使用有它自己的问题,详见异步控制台程序。async void方法最关键的用例是事件句柄。

Returning Values 返回值

异步方法返回类型为Task或者void,表示没有返回值,返回一个Task<T>表示必须返回一个T类型的值。

public async Task<int> CalculateAnswer()
{
await Task.Delay(100); // (Probably should be longer...) // Return "int"类型, 不是"Task<int>"
return 42;
}

虽然习惯起来有一点别扭,但这有一些背后设计的原因(异步CTP(Async CTP)为什么那样工作?

Context  上下文

当等待一个内置的异步操作时,内置的异步操作将会捕获当前的上下文context,然后会把它应用到这个异步方法的剩余部分。

那什么是context呢?

简单理解如下:

1.如果你正处于一个UI线程,那么它就是一个UI上下文。

2.如果你正在响应一个ASP.NET请求,那它就是一个ASP.NET请求上下文。

3.否则,它通常是一个线程池上下文。

复杂理解如下:

1.如果SynchronizationContext.Current不是null,那么它就是当前的SynchronizationContext.Current。(UI 和ASP.NET请求上下文都是SynchronizationContext 对象)

2.否则,它就是当前的TaskScheduler(TaskScheduler.Default是线程池上下文)。

在真实世界中,这意味着什么呢?首先,捕获(和存储)UI/ASP.NET上下文是透明的,即不可见的。

// WinForms 例子(对于 WPF同样有效).
private async void DownloadFileButton_Click(object sender,EventArgs e)
{
// 当我们异步等待的时候,UI线程没有被文件下载阻塞。
await DownloadFileAsync(fileNameTextBox.Text);// 因为我们还处于UI线程上,所以还可以直接访问UI元素。
resultTextBox.Text="File downloaded!";
}
// ASP.NET 例子
protected async void MyButton_Click(object sender,EventArgs e)
{
// 当我们异步等待的时候,ASP.NET线程没有被文件下载阻塞。
// 当我们等待的时候,就可以使用当前线程处理其他的请求。
await DownloadFileAsync(...);// 既然我们还在ASP.NET上下文上,我们就可以访问当前请求。
//虽然实际上我们可能在其他的线程上,但是我们仍有相同的ASP.NET请求上下文。
Response.Write("File downloaded!");
}
 

Avoiding Context:避免上下文

多数时候,你不需要同步返回到main上下文。记住:大多数异步方法被组合设计--他们等待其他多个操作,每一个操作本身代表一个异步操作(每个异步操作又可以被多个异步操作组合)。

这种情况下,你想要告诉异步者(awaiter)通过调用ConfigureAwait并且传递false不要捕捉当前上下文,比如:

private async TaskDownloadFileAsync(string fileName)
{
// 使用HttpClient或其他东西来下载文件内容。
var fileContents = await DownloadFileContentsAsync(fileName).ConfigureAwait(false);// 注意因为 ConfigureAwait(false),此时我们就不在原始的上下文了.
// 取而代之的是,我们正运行在线程池上.
// 将文件内容写入磁盘文件.
await WriteToDiskAsync(fileName, fileContents).ConfigureAwait(false);
}
// WinForms和 WPF均适用.这次没有调用ConfigureAwait(false)
private async void DownloadFileButton_Click(object sender, EventArgs e)
{
// 当我们异步等待的时候,ASP.NET线程没有被文件下载阻塞。
await DownloadFileAsync(fileNameTextBox.Text);// 因为我们还处于UI线程上,所以还可以直接访问UI元素。
resultTextBox.Text = "File downloaded!";
}

这个例子最重要的值得注意的是每一级的异步方法的调用都有自己的上下文。DownloadFileButton_Click开始是在UI线程上,然后调用了DownloadFileContentAsync,DownloadFileContentAsync一开始也在这个UI线程上。然后通过调用ConfigureAwait(false)跳出当前上下文。DownloadFileContentAsync剩余部分就运行在线程池上下文中了。然而,当DownloadFileContentAsync执行完成后,当DownloadFileButton_Click 恢复时,它(DownloadFileButton_Click )再次恢复到当前UI线程中了。

好的经验是:除非你知道你确实需要这个上下文,你可以使用ConfigureAwait(false)。

Async Composition 异步组合

至此,我们仅仅考虑了序列组合:一个异步方法一次等待一个操作。开始多个操作并且等待这些操作的一个(或全部)完成的情况也是可能的。

可以通过开始这些操作而不等待等待它们,直到以后再等待它们来达到这个目的。

public async Task DoOperationsConcurrentlyAsync()
{
Task[] tasks = new Task[3];
tasks[0] = DoOperation0Async();
tasks[1] = DoOperation1Async();
tasks[2] = DoOperation2Async(); //此时,所有的3个任务在同一时间运行。 // 现在,我们等待它们。
await Task.WhenAll(tasks);
} public async Task<int> GetFirstToRespondAsync()
{
// 调用2个web服务; 获取第一个响应.
Task<int>[] tasks = new[] { WebService1Async(), WebService2Async() }; // 等待第一个任务响应.
Task<int> firstTask = await Task.WhenAny(tasks); // Return结果.
return await firstTask;
}

通过使用并发组合 (Task.WhenAll 或Task.WhenAny),可以执行简单的并发操作。也可以伴随Task.Run来使用这些方法来做一些简单的并行计算。

然而,这不是任务并行库(TPL)的替代品,任何高级的CPU集中的并行操作应该使用TPL来完成。

Guidelines 指南

有兴趣的朋友可以读一下这篇文章 Task-based Asynchronous Pattern (TAP) document(链接地址:

http://www.microsoft.com/en-us/download/details.aspx?id=19957),是英文的,包括API设计的指导和具体使用async和await的场景,需要的话支持一下,我给大家翻译过来分享一下,谢谢。

Old

New

Description

task.Wait await task 等待任务完成
task.Result await task 获取完成任务的结果
Task.WaitAny await Task.WhenAny 等待任务集合中的任何一个完成
Task.WaitAll await Task.WhenAll 等待所有任务都完成
Thread.Sleep await Task.Delay 等待一段时间
Task constructor Task.Run or TaskFactory.StartNew 创建一个基于代码的任务

Next Steps 下一步

我这里还有一篇文章异步编程最佳实践,进一步解释了“避免async void”,总是”async”和”配置上下文”的指南。

微软的官文也相当不错official MSDN documentation。异步团队也发布了很多关于异步/等待的常见问题,这是继续学习异步的绝好去处。

文章翻译来源:http://blog.stephencleary.com/2012/02/async-and-await.html