是否有任何使用新的 async/await 功能编写 .NET API 的指南
我目前正在设计一些内部 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()
andvoid DoStuff()
- Should all async methods I expose be in the form
async Task<T> GetStuffAsync()
(i.e. method name end withAsync
) or is it ok to to have something namedGetStuff()
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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
很难找到有价值的内容,大多数出版物都集中在客户端编程方面。好东西是:
请记住,这都是预览版,重要细节可能会在发布之前发生变化。
Hard to find the nuggets, most publications are focused on the client programming side. Good stuff is:
Do keep in mind this is all preview, important details may change before this ships.
使用await实现异步API时要使用的一件重要事情是确保使用ConfigureAwait(false) 每当您想要在 API 实现中等待任务时。它的作用是允许 TPL 使用 TPL 默认行为(线程池)而不是 TaskAwaiter 的默认行为(当前同步上下文)来安排等待恢复。
对于使用者来说,使用当前同步上下文是正确的默认行为,因为如果您已经在 UI 线程上,它允许诸如等待返回 UI 线程之类的操作。但是,如果 UI 线程无法执行该方法的其余部分,则尝试返回 UI 线程可能会出现问题。
await
获取线程执行该方法的方式是在底层创建委托的标准 .NET 约定。然后,这些委托将被发送到以任何类型的调度机制(例如 WinForms 消息泵、WPF 调度程序或任何其他机制)进行处理。然而,对于 API 实现来说,尝试返回相同的上下文通常是错误的,因为这隐式地依赖于可用于执行的原始上下文。
例如,如果我在 UI 线程上有一些代码:
这种代码[b]非常[/b]很容易写,而且很容易导致挂起。典型的情况是,在
MyAsyncMethod()
内部,有一个使用默认同步上下文调度的await。这意味着在 UI 上下文中,将调用 DownloadSomethingAsync() 方法,然后开始下载。MyAsyncMethod()
然后测试await
操作数是否“完成”。假设下载尚未完成,那么await
的定义行为就是去掉该方法的“其余部分”,并安排在await
完成后执行该方法> 操作数确实已完成。因此...执行方法其余部分的状态被隐藏在委托中,现在 MyAsyncMethod() 将其自己的任务返回给
MyUIThreadCode()
。现在,
MyUIThreadCode()
对返回的任务调用Task.Wait()
。但问题是 .NET 中的Task
实际上是具有“完成”概念的任何事物的通用表示。仅仅因为您有一个Task
对象,就无法保证它将如何执行,也无法保证它将如何完成。如果您猜测的话,另一件事不能保证是它的隐式依赖关系。因此,在上面的示例中,
MyAsyncMethod()
在任务上使用默认的等待行为,该行为在当前上下文上安排方法延续。方法延续需要在MyAsyncMethod()
返回的任务被视为完成之前执行。但是,
MyUIThreadCode()
在任务上调用了Wait()
。定义的行为是阻止当前线程,将当前函数保留在堆栈上,并有效地等待任务完成。用户在这里没有意识到的是,他们被阻止的任务依赖于主动处理的 UI 线程,但它无法执行此操作,因为它仍在忙于执行在
Wait()
调用上被阻止的函数。Wait()
返回 (2)MyAsyncMethod()
的方法延续 (4)。明确了循环依赖关系,最终没有一个条件得到满足,并且 UI 线程实际上挂起。
以下是使用ConfigureAwait(false) 修复此问题的方法:
这里发生的情况是,
MyAsyncMethod()
的方法延续使用TPL 默认值(线程池),而不是当前的同步上下文。现在,该行为的条件如下:Wait()
返回 (2)MyAsyncMethod()
的方法延续 (4)。您已经打赌 .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:
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 theawait
operand is "done". Let's say that it isn't done downloading, so then the defined behavior forawait
is to carve away the "rest" of the method, and schedule that for execution once theawait
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 toMyUIThreadCode()
.Now
MyUIThreadCode()
callsTask.Wait()
on the returned task. But the issue is thatTask
in .NET is really a general purpose representation of anything that has the notion of "done-ness". Just because you have aTask
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 beforeMyAsyncMethod()
's returned task gets considered completed.However,
MyUIThreadCode()
calledWait()
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.Wait()
to return (2)MyAsyncMethod()
to be executed (4).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):
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:Wait()
to return (2)MyAsyncMethod()
to be executed (4).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!
我认为目前 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.