通过代码理解 await 和 async






4.51/5 (22投票s)
本文提供了一种代码优先的方法来理解 await/async,并回答了一些基本问题。
引言
有很多文章解释了 Await/Async 的目的以及它如何帮助简化异步编程。 然而,我总是对以下问题感到困惑:
- async/await 关键字会创建单独的线程吗?
- await 怎么挂起当前方法(调用线程被阻塞了吗)?
- 执行如何返回到调用方法(如果调用线程被阻塞)?
本文的重点是以一种简单易懂的方式回答这些问题。 本文面向希望使用 await/async 了解异步编程的开发人员。
背景
Async 和 Await 关键字是在 .Net 4.5 & C# 5.0 中添加的,用于简化异步编程。 目标是让开发人员更容易编写异步代码,并将重复性任务移交给编译器。
早期的异步编程模型 (APM) 基于 IAsyncResult,其中异步操作需要 Begin 和 End 方法。 例如,FileStream 类实现了 BeginRead 和 EndRead 方法。
public override IAsyncResult BeginRead(byte[] array,int offset,int numBytes,AsyncCallback userCallback, Object stateObject) public override int EndRead(IAsyncResult asyncResult)
新的异步编程模型基于 Task 类型,该类型使用单个操作来表示异步操作的开始和完成。 BeginRead 和 EndRead 方法已被新的 ReadAsync 方法取代。
public Task<int> ReadAsync(byte [] buffer, int offset, int count);
基于任务的异步模式被称为 TAP。
既然我们已经奠定了基础,现在是时候讨论 await/async 的用法以及它们如何用于简化基于任务的异步编程。
代码优先
与其从一个例子开始并在其细节中迷失方向,不如我们直接查看使用 await 关键字的代码。
int sum = await AddAsync(x, y);
int multiply = sum * z;
return multiply;
上面的代码调用了一个返回两个数字之和的方法。 add 方法本质上是异步的,并返回一个 Task<int>
。 该方法的签名如下所示:
public async Task<int> AddAsync(int x, int y)
正如您可能已经读到的,await 关键字会挂起当前方法的执行并将控制权返回给调用者。 该关键字不会创建任何线程。 那么它是怎么做到的呢?
来自 MSDN:
引用异步方法中的 await 表达式不会在等待的任务运行时阻塞当前线程。 相反,该表达式将该方法的其余部分注册为延续,并将控制权返回给异步方法的调用者。
为了演示,我们可以重写原始代码而不使用 await,如下所示:
var t = AddAsync(x, y); var t2= t.ContinueWith((task) => { int sum = t.Result; int multiply = sum * z; return multiply; },TaskScheduler.FromCurrentSynchronizationContext()); return t2;
await 关键字不会创建额外的线程,因为它在当前的同步上下文中运行。
这确实回答了两个重要问题。
- 没有创建额外的线程。
- 剩余的表达式被注册为在任务完成后继续执行。
await 关键字完成了大部分魔术。 添加 async 关键字主要是为了在使用 await 关键字时避免向后兼容性问题。 根据 MSDN 的一篇博文:
引用要求 "async" 意味着我们可以一次性消除所有向后兼容性问题; 任何包含 await 表达式的方法都必须是 "新构造" 代码,而不是 "旧工作" 代码,因为 "旧工作" 代码从没有 async 修饰符。
http://blogs.msdn.com/b/ericlippert/archive/2010/11/11/whither-async.aspx
然而,async 关键字确实有一个诀窍。
public async Task<int> AddAndMultiplyAsync(int x, int y, int z) { int sum = await AddAsync(x, y); int multiply = sum * z; return multiply; }
查看上面的方法,我们看到返回类型是 Task<int>,但是我们返回的是一个整数。 当然,魔术在于 async 关键字,它会自动将返回值更改为 Task<T>,其中 T 是我们返回的类型。 如果您正在使用 async,您可能已经看到了这个错误弹出来:
error CS4016: Since this is an async method, the return expression must be of type 'int' rather than 'Task<int>'
这是因为如果您尝试在 async 方法中返回 Task,则返回类型应为 Task<Task<T>>。
public async Task<Task<int>> AddAndMultiplyAsync(int x, int y, int z) { return AddAsync(x, y); }
摘要
Async/Await 会创建单独的线程吗?
不会 – async 自动将返回值更改为 Task,并允许我们使用 await 关键字。 await 将剩余方法注册为延续并将控制权返回给异步方法的调用者。
Await 如何挂起当前方法?
当我们使用 await 关键字时,它将方法调用的其余部分包装在一个 Task.ContinueWith 块中。
执行如何返回到调用方法?
由于当前方法调用被包装在一个 Task.ContinueWith 块中,await 关键字返回 Task<T>
历史
- 2014 年 8 月 9 日 - 草稿版本。
- 2014 年 8 月 9 日 - 添加背景