是否有任何使用新的 async/await 功能编写 .NET API 的指南

发布于 2024-12-12 04:30:54 字数 436 浏览 0 评论 0原文

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

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

具体来说:

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

?这里并非全黑或白,这取决于所讨论的方法,但我正在寻找一般准则。

I'm currently designing some internal APIs where I use the Async CTP and its new await/async keywords.

Are there any guidelines or best practices on how these APIs should be designed?

Specifically:

  • Should I provide both a synchronous and an asynchronous version of methods? (i.e. Task DoStuffAsync() and void DoStuff()
  • Should all async methods I expose be in the form async Task<T> GetStuffAsync() (i.e. method name end with Async) or is it ok to to have something named GetStuff() be awaitable?

I do understand that it's not all black or white here, and that it depends on the method in question, but I'm looking for general guidelines.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

双手揣兜 2024-12-19 04:30:54

很难找到有价值的内容,大多数出版物都集中在客户端编程方面。好东西是:

  • 标题为“基于任务的异步模式”的白皮书。下载 在这里(注意:archive.org 备份,下载按钮截至 2024 年 5 月仍然有效)。
  • Stephen Toub 在 Windows Build 上做了一次演讲,讨论了优化模式和许多重要的提示。有一个视频< /a>,也可用打印

请记住,这都是预览版,重要细节可能会在发布之前发生变化。

Hard to find the nuggets, most publications are focused on the client programming side. Good stuff is:

  • A white paper titled The Task-based Asynchronous Pattern. Download is here (note: archive.org backup, Download button still works as of May 2024).
  • Stephen Toub had a presentation at Windows Build that talked about optimizing the pattern, many important hints. There's a video, also available in print.

Do keep in mind this is all preview, important details may change before this ships.

天邊彩虹 2024-12-19 04:30:54

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

对于使用者来说,使用当前同步上下文是正确的默认行为,因为如果您已经在 UI 线程上,它允许诸如等待返回 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()。但问题是 .NET 中的 Task 实际上是具有“完成”概念的任何事物的通用表示。仅仅因为您有一个 Task 对象,就无法保证它将如何执行,也无法保证它将如何完成。如果您猜测的话,另一件事不能保证是它的隐式依赖关系。

因此,在上面的示例中,MyAsyncMethod() 在任务上使用默认的等待行为,该行为在当前上下文上安排方法延续。方法延续需要在 MyAsyncMethod() 返回的任务被视为完成之前执行。

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

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

  1. 为了让 MyUIThreadCode() 完成其方法调用,它需要 Wait() 返回 (2)
  2. 为了让 Wait() 返回,它需要 asyncTask 来完成 (3)。
  3. 为了完成 asyncTask,需要执行 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,需要执行 MyAsyncMethod() 的方法延续 (4)。
  4. (新) 为了让方法延续运行,它需要线程池进行处理 (5)。
  5. 实际上,线程池始终在处理。事实上,.NET 线程池经过精心调整,具有高可扩展性(动态分配和退出线程)以及低延迟(它有一个最大阈值,允许请求在继续并开始之前变得过时)一个新线程以确保保留吞吐量)。

您已经打赌 .NET 已经成为一个可靠的平台,并且我们非常重视 .NET 中的线程池。

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

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

其他事物(例如接口契约或虚拟方法契约)通常有特定的承诺,即在该方法返回之前执行某些事情。除非您的接口或虚拟方法返回一个任务...您可能需要对调用的任何异步 API 进行一些阻止。在这种情况下,这实际上违背了异步的目的......但也许您可以从不同代码路径中的异步中受益。

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

希望这有帮助!

One crucial thing to use when using await to implement async APIs is to make sure to use ConfigureAwait(false) whenever you want to await a task inside the API impl. What that does is allow for the TPL to schedule your await resumption using the TPL default behavior (threadpool), rather than the TaskAwaiter's default behavior (current sync context).

Using the current sync context is the right default behavior for consumers because it allows for things such as await returning to the UI thread, if you were already on the UI thread. However, trying to come back to the UI thread can have problems if the UI thread isn't available to execute the rest of the method. The way await gets threads to execute the method is the standard .NET convention of creating delegates under the hood. These delegates then get sent to be processed in whatever sort of dispatching mechanism (e.g. WinForms message pump, WPF dispatcher, or anything else).

However, trying to come back to the same context is typically the wrong thing for API implementations, because that implicitly takes a dependency on that original context being available for execution.

For example, if I have some code on the UI thread:

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

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

This kind of code is [b]very[/b] tempting to write, and very easy to cause hangs. The typical case is that inside MyAsyncMethod(), there is an await that uses default sync context scheduling. That means that in the UI context, the DownloadSomethingAsync() method will get called, and the downloading will begin.

MyAsyncMethod() then tests if the await operand is "done". Let's say that it isn't done downloading, so then the defined behavior for await is to carve away the "rest" of the method, and schedule that for execution once the await operand really is done.

So... the state for executing the rest of the method gets stashed away in a delegate, and now MyAsyncMethod() returns back its own task to MyUIThreadCode().

Now MyUIThreadCode() calls Task.Wait() on the returned task. But the issue is that Task in .NET is really a general purpose representation of anything that has the notion of "done-ness". Just because you have a Task object, there is nothing to guarantee how it's going to execute, nor how is it going to reach completion. If you're guessing, another thing that is not guaranteed are its implicit dependencies.

So in the above example, MyAsyncMethod() uses the default await behavior on a Task, which schedules method continuations on the current context. The method continuation needs to execute before MyAsyncMethod()'s returned task gets considered completed.

However, MyUIThreadCode() called Wait() on the task. The defined behavior there is to BLOCK the current thread, keep the current function on the stack, and effectively wait until the task is done.

What the user didn't realize here was that the task they are blocked on relies on the UI thread actively processing, which it can't do because it's still busy executing the function that is blocked on the Wait() call.

  1. In order for MyUIThreadCode() to finish its method call, it needs for Wait() to return (2)
  2. In order for Wait() to return, it needs asyncTask to complete (3).
  3. In order for asyncTask to complete, it needs for the method continuation from MyAsyncMethod() to be executed (4).
  4. In order for the method continuation to get run, it needs the message loop to be processing (5).
  5. In order for the message loop to continue processing, it needs MyUIThreadCode() to return (1).

There's the circular dependency spelled out, none of the conditions end up being satisfied, and effectively the UI thread hangs.

Here's how you fix it with ConfigureAwait(false):

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

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

What happens here is that the method continuation for MyAsyncMethod() uses the TPL default (threadpool), rather than the current sync context. And here are the conditions now, with that behavior:

  1. In order for MyUIThreadCode() to finish its method call, it needs for Wait() to return (2)
  2. In order for Wait() to return, it needs asyncTask to complete (3).
  3. In order for asyncTask to complete, it needs for the method continuation from MyAsyncMethod() to be executed (4).
  4. (NEW) In order for the method continuation to get run, it needs the thread pool to be processing (5).
  5. Effectively, the threadpool is always processing. In fact, the .NET threadpool is very much tuned to have high scalability (dynamically allocates and retires threads), as well as low latency (it has a max threshold that it allows for a request to get stale, before it goes ahead and starts a new thread to ensure throughput is preserved).

You're making a bet on .NET being a solid platform already, and we take the thread pool very seriously in .NET.

So, one might ask perhaps the problem is with the Wait() call... why did they use a blocking wait to begin with?

The answer is that sometimes you really just have to. For example, the .NET contract for Main() methods is that the program is terminated when the Main() method returns. Or... in other words, the Main() method blocks until your program is done.

Other things like interface contracts, or virtual method contracts typically have specific promises that certain things are performed before that method returns. Unless your interface or virtual method gives back a Task... you'll likely need to do some blocking on any async APIs that are called. This effectively defeats the purpose of async in that one situation... but perhaps you benefit from the asynchrony in a different codepath.

Thus for API providers that return async Tasks, by using ConfigureAwait(false), you are helping ensure that your returned Tasks don't have any unexpected implicit dependencies (e.g. like the UI message loop is still actively pumping). The more you can contain your dependencies, the better an API you are.

Hope this helps!

蛮可爱 2024-12-19 04:30:54

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

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

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

I don't think that there are best practices for the CTP right now.

You can provide both methods, but you should ask yourself:
Does it make sence to call a method async?

I would limit it to time-expensiv methods only. If you need to make another method async, which has no method which returns a task, you still can create a small wrapper around the method.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文