.NET 基于任务的异步模式(Task-based Asynchronous Pattern,TAP) async await
本文内容
- 概述
- 编写异步方法
- 异步程序中的控制流
- API 异步方法
- 线程
- 异步和等待
- 返回类型和参数
- 参考资料
下载 Demo
下载 Demo TPL 与 APM 和 EAP 结合(APM 和 EAP 这两个标准异步方式已经不能适应多核时代,但之前用这两种方式写的代码怎么办?——把它们改造一下,跟 TPL 结合)
概述
异步对可能起阻止作用的活动(例如,应用程序访问 Web 时)至关重要。 对 Web 资源的访问有时很慢或会延迟。 如果此类活动在同步过程中受阻,则整个应用程序必须等待。在异步过程中,应用程序可继续执行不依赖 Web 资源的其他工作,直至潜在阻止任务完成。
下表是利用异步编程能提高响应能力的典型场景。从 .NET Framework 4.5 和 Windows 运行时中列出的 API 包含支持异步编程的方法。
应用程序区域 | 包含异步方法的受支持的 API |
Web 访问 | HttpClient ,SyndicationClient |
使用文件 | StorageFile、StreamWriter、StreamReader、XmlReader |
使用图像 | MediaCapture、BitmapEncoder、BitmapDecoder |
WCF 编程 | 同步和异步操作 |
由于所有与用户界面相关的活动通常共享一个线程,因此,异步对访问 UI 线程的应用程序来说尤为重要。如果任何进程在同步应用程序中受阻,则所有进程都将受阻。 你的应用程序停止响应,因此,你可能在其等待过程中认为它已经失败。
使用异步方法时,应用程序将继续响应 UI。 例如,你可以调整窗口的大小或最小化窗口;如果你不希望等待应用程序结束,则可以将其关闭。
可以使用三种方式来实现 TAP:即手动使用 C# 编译器,或将编译器和手动方法结合使用。使用 TAP 模式来实现计算密集型和 I/O 密集型异步操作。
- 使用编译器。在 Visual Studio 2012 和 .NET Framework 4.5 中,任何具有 async 关键字的方法都被看作是一种异步方法,并且 C# 会执行必要的转换,以通过 TAP 来异步实现该方法。 异步方法应返回 System.Threading.Tasks.Task 或 System.Threading.Tasks.Task<TResult> 对象。
- 手动生成 TAP 方法。也可以手动实现 TAP 模式,以更好地控制实现。编译器依赖从 System.Threading.Tasks 命名空间公开的公共外围应用和 System.Runtime.CompilerServices 命名空间中支持的类型。 如要自己实现 TAP,你需要创建一个 TaskCompletionSource<TResult> 对象、执行异步操作,并在操作完成时,调用 SetResult、SetException、SetCanceled 方法,或调用这些方法之一的Try版本。 手动实现 TAP 方法时,需在所表示的异步操作完成时完成生成的任务。 例如:
- 混合方法。你可能发现手动实现 TAP 模式、但将实现核心逻辑委托给编译器的这种方法很有用。 例如,当你想要验证编译器生成的异步方法之外的实参时,可能需要使用这种混合方法,以便异常可以转义到该方法的直接调用方而不是通过 System.Threading.Tasks.Task 对象被公开:
public static Task<int> ReadTask(this Stream stream, byte[] buffer, int offset, int count, object state)
{
var tcs = new TaskCompletionSource<int>();
stream.BeginRead(buffer, offset, count, ar =>
{
try { tcs.SetResult(stream.EndRead(ar)); }
catch (Exception exc) { tcs.SetException(exc); }
}, state);
return tcs.Task;
}
public Task<int> MethodAsync(string input)
{
if (input == null) throw new ArgumentNullException("input");
return MethodAsyncInternal(input);
}
private async Task<int> MethodAsyncInternal(string input)
{
// code that uses await goes here
return value;
}
本文主要说明“使用编译器”方法。
编写异步方法
C# 中 async 和 await 关键字是异步编程的核心。通过这两个关键字就可以轻松创建异步方法,几乎与创建同步方法一样。如下所示的 WPF 程序,布局文件上有个按钮和文本框:
private async void StartButton_Click(object sender, RoutedEventArgs e)
{
// Call and await separately.
//Task<int> getLengthTask = AccessTheWebAsync();
//// You can do independent work here.
//int contentLength = await getLengthTask;
int contentLength = await AccessTheWebAsync();
resultsTextBox.Text +=
String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength);
}
// Three things to note in the signature:
// - The method has an async modifier.
// - The return type is Task or Task<T>. (See "Return Types" section.)
// Here, it is Task<int> because the return statement returns an integer.
// - The method name ends in "Async."
async Task<int> AccessTheWebAsync()
{
// You need to add a reference to System.Net.Http to declare client.
HttpClient client = new HttpClient();
// GetStringAsync returns a Task<string>. That means that when you await the
// task you'll get a string (urlContents).
Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");
// You can do work here that doesn't rely on the string from GetStringAsync.
DoIndependentWork();
// The await operator suspends AccessTheWebAsync.
// - AccessTheWebAsync can't continue until getStringTask is complete.
// - Meanwhile, control returns to the caller of AccessTheWebAsync.
// - Control resumes here when getStringTask is complete.
// - The await operator then retrieves the string result from getStringTask.
string urlContents = await getStringTask;
// The return statement specifies an integer result.
// Any methods that are awaiting AccessTheWebAsync retrieve the length value.
return urlContents.Length;
}
void DoIndependentWork()
{
resultsTextBox.Text += "Working . . . . . . .\r\n";
}
执行结果:
Working . . . . . . .
Length of the downloaded string: 41609.
说明:
1,当程序访问网络时,无论你如何拖拽、最大化最小化、如何点击,UI 都不会失去响应;
2,“async Task<int> AccessTheWebAsync()”方法签名,有三点需要注意:1)有 async 修饰符;2)返回类型是 Task 或 Task<int>。该方法是 Task<int>,因为它返回的是链接内容的大小;3)方法名以 Async 结尾;
3,“string urlContents = await getStringTask;”语句,有四点需要注意:1)AccessTheWebAsync 方法直到 getStringTask 完成才能继续;2)同时,控制流返回到 AccessTheWebAsync 的调用者;3)getStringTask 完成后,控制流才会恢复;4)之后,await 操作符从 getStringTask 检索结果。
下面总结让一个示例成为异步方法的特征:
- 方法签名包含一个 async 修饰符。
- 按照约定,异步方法的名称以“Async”后缀结尾。
- 返回类型为下列类型之一:
- 如果你的方法有 TResult 类型的返回语句,则为 Task<TResult>。
- 如果你的方法没有返回语句,则为 Task。
- 如果你编写的是异步事件处理程序,则为 Void(Visual Basic 中为 Sub)。
- 方法通常包含至少一个 await 表达式,该表达式标记一个点,在该点上,直到等待的异步操作完成方法才能继续。同时,将方法挂起,并且控件返回到方法的调用方。
在异步方法中,可使用提供的关键字和类型来指示需要完成的操作,且编译器会完成其余操作,其中包括持续跟踪控件以挂起方法返回等待点时发生的情况。 一些常规流程(例如,循环和异常处理)在传统异步代码中处理起来可能很困难。 在异步方法中,元素的编写频率与同步解决方案相同且此问题得到解决。
异步程序中的控制流
异步编程中最需弄清的是控制流是如何从方法移动到方法。
private async void StartButton_Click(object sender, RoutedEventArgs e)
{
// Call and await separately.
//Task<int> getLengthTask = AccessTheWebAsync();
//// You can do independent work here.
//int contentLength = await getLengthTask;
resultsTextBox.Text += "1: Entering startButton_Click.\r\n" +
" Calling AccessTheWebAsync.\r\n";
int contentLength = await AccessTheWebAsync();
resultsTextBox.Text +=
String.Format("\r\n6: Length of the downloaded string: {0}.\r\n", contentLength);
}
async Task<int> AccessTheWebAsync()
{
resultsTextBox.Text += "\r\n2: Entering AccessTheWebAsync.";
HttpClient client = new HttpClient();
resultsTextBox.Text += "\r\n Calling HttpClient.GetStringAsync.\r\n";
Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");
DoIndependentWork();
resultsTextBox.Text += "\r\n4: Back in startButton_Click.\r\n" +
" Task getStringTask is started.\r\n";
string urlContents = await getStringTask;
resultsTextBox.Text += "\r\n5: Back in AccessTheWebAsync." +
"\r\n Task getStringTask is complete." +
"\r\n Processing the return statement." +
"\r\n Exiting from AccessTheWebAsync.\r\n";
return urlContents.Length;
}
void DoIndependentWork()
{
resultsTextBox.Text += "\r\n3: Entering DoIndependentWork.\r\n";
resultsTextBox.Text += "\r\n Working . . . . . . .\r\n";
}
运行结果:
1: Entering startButton_Click.
Calling AccessTheWebAsync.
2: Entering AccessTheWebAsync.
Calling HttpClient.GetStringAsync.
3: Entering DoIndependentWork.
Working . . . . . . .
4: Back in startButton_Click.
Task getStringTask is started.
5: Back in AccessTheWebAsync.
Task getStringTask is complete.
Processing the return statement.
Exiting from AccessTheWebAsync.
6: Length of the downloaded string: 41609.
再稍微复杂点:
private async void startButton_Click(object sender, RoutedEventArgs e)
{
// The display lines in the example lead you through the control shifts.
resultsTextBox.Text += "ONE: Entering startButton_Click.\r\n" +
" Calling AccessTheWebAsync.\r\n";
Task<int> getLengthTask = AccessTheWebAsync();
resultsTextBox.Text += "\r\nFOUR: Back in startButton_Click.\r\n" +
" Task getLengthTask is started.\r\n" +
" About to await getLengthTask -- no caller to return to.\r\n";
int contentLength = await getLengthTask;
resultsTextBox.Text += "\r\nSIX: Back in startButton_Click.\r\n" +
" Task getLengthTask is finished.\r\n" +
" Result from AccessTheWebAsync is stored in contentLength.\r\n" +
" About to display contentLength and exit.\r\n";
resultsTextBox.Text +=
String.Format("\r\nLength of the downloaded string: {0}.\r\n", contentLength);
}
async Task<int> AccessTheWebAsync()
{
resultsTextBox.Text += "\r\nTWO: Entering AccessTheWebAsync.";
// Declare an HttpClient object and increase the buffer size. The default
// buffer size is 65,536.
HttpClient client =
new HttpClient() { MaxResponseContentBufferSize = 1000000 };
resultsTextBox.Text += "\r\n Calling HttpClient.GetStringAsync.\r\n";
// GetStringAsync returns a Task<string>.
Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");
resultsTextBox.Text += "\r\nTHREE: Back in AccessTheWebAsync.\r\n" +
" Task getStringTask is started.";
// AccessTheWebAsync can continue to work until getStringTask is awaited.
resultsTextBox.Text +=
"\r\n About to await getStringTask and return a Task<int> to startButton_Click.\r\n";
// Retrieve the website contents when task is complete.
string urlContents = await getStringTask;
resultsTextBox.Text += "\r\nFIVE: Back in AccessTheWebAsync." +
"\r\n Task getStringTask is complete." +
"\r\n Processing the return statement." +
"\r\n Exiting from AccessTheWebAsync.\r\n";
return urlContents.Length;
}
运行结果:
ONE: Entering startButton_Click.
Calling AccessTheWebAsync.
TWO: Entering AccessTheWebAsync.
Calling HttpClient.GetStringAsync.
THREE: Back in AccessTheWebAsync.
Task getStringTask is started.
About to await getStringTask and return a Task<;int> to startButton_Click.
FOUR: Back in startButton_Click.
Task getLengthTask is started.
About to await getLengthTask -- no caller to return to.
FIVE: Back in AccessTheWebAsync.
Task getStringTask is complete.
Processing the return statement.
Exiting from AccessTheWebAsync.
SIX: Back in startButton_Click.
Task getLengthTask is finished.
Result from AccessTheWebAsync is stored in contentLength.
About to display contentLength and exit.
Length of the downloaded string: 41635.
API 异步方法
如何找到像 GetStringAsync 这样支持异步编程的方法。 .NET Framework 4.5 包含使用 async 和 await 的许多成员,它们都已“Async”为后缀和 Task 或 Task<TResult> 的返回类型。 例如,System.IO.Stream 类包含的方法 CopyToAsync、ReadAsync、WriteAsync 等方法以及同步方法 CopyTo、Read 和 Write。
线程
异步方法旨在成为非阻止操作。异步方法中的 await 表达式在等待的任务正在运行时,不会阻止当前线程。相反,表达式在继续时,注册方法的其余部分并将控件返回到异步方法的调用方。
async 和 await 关键字不会导致创建其他线程。因为异步方法不会在其自身线程上运行,因此它不需要多线程。 只有当方法处于活动状态时,该方法将在当前同步上下文中运行并使用线程上的时间。 可以使用 Task.Run 将占用大量 CPU 的工作移到后台线程,但是后台线程不会帮助正在等待结果的进程变为可用状态。
对于异步编程而言,该基于异步的方法优于几乎每个用例中的现有方法。 具体而言,此方法比 BackgroundWorker 更适用于 IO 绑定的操作,因为此代码更简单且无需防止争用条件。 结合 Task.Run 使用时,异步编程比 BackgroundWorker 更适用于 CPU 绑定的操作,因为异步编程将运行代码的协调细节与 Task.Run 传输至线程池的工作区分开来。
异步和等待
如果通过 async 修饰符指定某种方法为异步方法,则可以启用以下两个功能。
- 标记的异步方法可以使用 await 来指定悬挂点。await 运算符通知编译器异步方法只有直到等待的异步过程完成才能继续通过该点。 同时,控件返回至异步方法的调用方。 await 表达式中异步方法的挂起不能使该方法退出,并且 finally 块不会运行。
- 标记的异步方法本身可以通过调用它的方法等待。
异步方法通常包含 await 运算符的一个或多个匹配项,但缺少 await 表达式不会导致编译器错误。 如果异步方法未使用 await 运算符标记悬挂点,则该方法将作为同步方法执行,不管异步修饰符如何。编译器将为此类方法发布一个警告。
Async 、async、Await 和 await 都是上下文关键字。 有关更多信息和示例,请参见以下主题:
返回类型和参数
.NET Framework 异步编程中异步方法通常返回 Task 或 Task<TResult>。 在异步方法中,await 运算符应用于通过调用另一个异步方法返回的任务。
如果方法包含 Return (Visual Basic) 或指定类型 TResult 的操作数的 return (C#) 语句,则将 Task<TResult> 指定为返回类型。
如果方法不含任何 return 语句或包含不返回操作数的 return 语句,则将 Task 用作返回类型。
下面的示例演示如何声明并调用可返回 Task<TResult> 或 Task 的方法。
// Signature specifies Task<;TResult>
async Task<;int> TaskOfTResult_MethodAsync()
{
int hours;
// . . .
// Return statement specifies an integer result.
return hours;
}
// Calls to TaskOfTResult_MethodAsync
Task<;int> returnedTaskTResult = TaskOfTResult_MethodAsync();
int intResult = await returnedTaskTResult;
// or, in a single statement
int intResult = await TaskOfTResult_MethodAsync();
// Signature specifies Task
async Task Task_MethodAsync()
{
// . . .
// The method has no return statement.
}
// Calls to Task_MethodAsync
Task returnedTask = Task_MethodAsync();
await returnedTask;
// or, in a single statement
await Task_MethodAsync();
每个返回的任务表示正在进行的工作。 任务可封装有关异步进程状态的信息,如果未成功,则最后会封装来自进程的最终结果或进程引发的异常。
异步方法还可以是 Sub 方法 (Visual Basic) 或具有 void 返回类型 (C#)。 该返回类型主要用于定义需要 void 返回类型的事件处理程序。 异步事件处理程序通常用作异步程序的起始点。
无法等待为 Sub 程序或具有 void 返回类型的异步方法,并且无效的返回方法的调用方无法捕获该方法引发的任何异常。
异步方法无法声明 Visual Basic 中的 ByRef 参数或 C# 中的 ref 或 out 参数,但此方法可以调用具有此类参数的方法。
有关更多信息和示例,请参见异步返回类型(C# 和 Visual Basic)。 有关如何在异步方法中捕捉异常的更多信息,请参见 try-catch(C# 参考)或 Try...Catch...Finally 语句 (Visual Basic)。
Windows 运行时编程中的异步 API 具有下列返回类型之一,它类似于任务:
- IAsyncOperation,它对应于 Task<TResult>
- IAsyncAction,它对应于 Task
- IAsyncActionWithProgress
- IAsyncOperationWithProgress
参考资料