65.9K
CodeProject 正在变化。 阅读更多。
Home

通过代码理解 await 和 async

2014年8月9日

CPOL

3分钟阅读

viewsIcon

55493

本文提供了一种代码优先的方法来理解 await/async,并回答了一些基本问题。

引言

有很多文章解释了 Await/Async 的目的以及它如何帮助简化异步编程。 然而,我总是对以下问题感到困惑:

  • async/await 关键字会创建单独的线程吗?
  • await 怎么挂起当前方法(调用线程被阻塞了吗)?
  • 执行如何返回到调用方法(如果调用线程被阻塞)?

本文的重点是以一种简单易懂的方式回答这些问题。 本文面向希望使用 await/async 了解异步编程的开发人员。

背景

Async 和 Await 关键字是在 .Net 4.5 & C# 5.0 中添加的,用于简化异步编程。 目标是让开发人员更容易编写异步代码,并将重复性任务移交给编译器。

早期的异步编程模型 (APM) 基于 IAsyncResult,其中异步操作需要 Begin 和 End 方法。 例如,FileStream 类实现了 BeginReadEndRead 方法。

public override IAsyncResult BeginRead(byte[] array,int offset,int numBytes,AsyncCallback userCallback,   Object stateObject)

public override int EndRead(IAsyncResult asyncResult)

新的异步编程模型基于 Task 类型,该类型使用单个操作来表示异步操作的开始和完成。 BeginReadEndRead 方法已被新的 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>

历史

  1. 2014 年 8 月 9 日 - 草稿版本。
  2. 2014 年 8 月 9 日 - 添加背景

 

© . All rights reserved.