7

我目前正在设计一些使用 Async CTP 及其新await/async关键字的内部 API。

是否有关于如何设计这些 API 的指南或最佳实践?

具体来说:

  • 我应该同时提供同步和异步版本的方法吗?(即Task DoStuffAsync() void DoStuff()
  • 我公开的所有异步方法都应该采用这种形式async Task<T> GetStuffAsync()(即方法名称以 结尾Async)还是可以让命名为 GetStuff()可等待的东西?

我明白这里并非全是黑或白,这取决于所讨论的方法,但我正在寻找一般指导方针。

4

3 回答 3

9

很难找到掘金,大多数出版物都集中在客户端编程方面。好东西是:

  • 一份名为“基于任务的异步模式”的白皮书。下载在这里(注意:今天早上回复慢)。
  • Stephen Toub 在 Windows Build 上做了一个演讲,谈到了优化模式和许多重要提示。有一个视频,也有印刷版

请记住,这是所有预览,重要细节可能会在此发布之前更改。

于 2011-10-26T14:16:24.777 回答
3

使用 await实现异步 API时要使用的一件关键事情是确保在 API impl 中等待任务时使用ConfigureAwait(false) 。这样做是允许 TPL 使用 TPL 默认行为(线程池)而不是 TaskAwaiter 的默认行为(当前同步上下文)来安排您的等待恢复。

使用当前同步上下文对于消费者来说是正确的默认行为,因为如果您已经在 UI 线程上,它允许诸如 await 返回到 UI 线程之类的事情。但是,如果 UI 线程无法执行该方法的其余部分,则尝试返回 UI 线程可能会出现问题。让await线程执行该方法的方式是在底层创建委托的标准 .NET 约定。然后将这些委托发送到任何类型的调度机制(例如 WinForms 消息泵、WPF 调度程序或其他任何东西)中进行处理。

然而,对于 API 实现来说,试图回到相同的上下文通常是错误的,因为这隐含地依赖于可以执行的原始上下文。

例如,如果我在 UI 线程上有一些代码:

void MyUIThreadCode() {
    Task asyncTask = MyAsyncMethod();
    asyncTask.Wait();
}

async Task MyAsyncMethod() {
    await DownloadSomethingAsync();
    ComputeSomethingElse();
}

这种代码写起来[b]非常[/b],而且很容易导致挂起。典型的情况是,在内部MyAsyncMethod(),有一个使用默认同步上下文调度的 await。这意味着在 UI 上下文中,将调用 DownloadSomethingAsync() 方法,然后开始下载。

MyAsyncMethod()然后测试await操作数是否“完成”。假设它没有完成下载,那么定义的行为await就是剥离方法的“其余部分”,并在操作数真正完成后安排await执行。

所以......执行该方法其余部分的状态被隐藏在委托中,现在MyAsyncMethod()将其自己的任务返回给MyUIThreadCode().

现在MyUIThreadCode()调用Task.Wait()返回的任务。但问题是,Task在 .NET 中,它实际上是任何具有“完成性”概念的通用表示。仅仅因为你有一个Task对象,就无法保证它会如何执行,也无法保证它会如何完成。如果您猜对了,另一件不能保证的事情是它的隐式依赖关系。

所以在上面的例子中,MyAsyncMethod()在一个任务上使用了默认的等待行为,它在当前上下文中调度方法继续。方法延续需要在MyAsyncMethod()返回的任务被认为完成之前执行。

但是MyUIThreadCode()调用Wait()了任务。定义的行为是阻塞当前线程,将当前函数保留在堆栈上,并有效地等待任务完成。

用户在这里没有意识到的是,他们被阻塞的任务依赖于 UI 线程主动处理,它无法做到这一点,因为它仍在忙于执行Wait()调用阻塞的函数。

  1. 为了让 MyUIThreadCode() 完成它的方法调用,它需要Wait()返回 (2)
  2. 为了让 Wait() 返回,它需要 asyncTask 完成 (3)。
  3. 为了使 asyncTask 完成,它需要执行方法 continuation MyAsyncMethod()(4)。
  4. 为了让方法继续运行,它需要处理消息循环 (5)。
  5. 为了让消息循环继续处理,它需要 MyUIThreadCode() 返回 (1)。

阐明了循环依赖关系,最终没有一个条件得到满足,并且 UI 线程实际上挂起。

以下是使用 ConfigureAwait(false) 修复它的方法:

void MyUIThreadCode() {
    Task asyncTask = MyAsyncMethod();
    asyncTask.Wait();
}

async Task MyAsyncMethod() {
    await DownloadSomethingAsync().ConfigureAwait(false);
    ComputeSomethingElse();
}

这里发生的是方法延续MyAsyncMethod()使用 TPL 默认(线程池),而不是当前同步上下文。这是现在的条件,具有这种行为:

  1. 为了让 MyUIThreadCode() 完成它的方法调用,它需要Wait()返回 (2)
  2. 为了让 Wait() 返回,它需要 asyncTask 完成 (3)。
  3. 为了使 asyncTask 完成,它需要执行方法 continuation MyAsyncMethod()(4)。
  4. (新)为了使方法继续运行,它需要线程池正在处理(5)。
  5. 实际上,线程池始终在处理。事实上,.NET 线程池非常适合具有高可伸缩性(动态分配和停用线程)以及低延迟(它有一个最大阈值,它允许请求在继续并开始之前变得陈旧一个新线程以确保保留吞吐量)。

您正在押注 .NET 已经成为一个可靠的平台,我们非常重视 .NET 中的线程池。

所以,有人可能会问,问题可能出在Wait()调用上……为什么他们一开始就使用阻塞等待?

答案是有时你真的不得不这样做。例如,Main() 方法的 .NET 约定是程序在 Main() 方法返回时终止。或者...换句话说,Main() 方法会阻塞,直到您的程序完成。

接口契约或虚拟方法契约等其他事物通常具有特定的承诺,即在该方法返回之前执行某些事情。除非您的接口或虚拟方法返回一个任务......您可能需要对任何被调用的异步 API 进行一些阻止。这有效地破坏了在那种情况下异步的目的......但也许你会从不同代码路径中的异步中受益。

因此,对于返回异步任务的 API 提供者,通过使用 ConfigureAwait(false),您可以帮助确保您返回的任务没有任何意外的隐式依赖项(例如,UI 消息循环仍在主动泵送)。您可以包含的依赖项越多,您的 API 就越好。

希望这可以帮助!

于 2011-10-27T10:21:26.163 回答
1

我认为目前没有 CTP 的最佳实践。

您可以同时提供这两种方法,但您应该问自己:异步调用方法是否有意义?

我只会将其限制为耗时的方法。如果您需要使另一个方法异步,它没有返回任务的方法,您仍然可以围绕该方法创建一个小包装器。

于 2011-10-26T14:03:58.373 回答