Microsoft建议在.NET中使用异步调用的最佳实践。在这些建议中,让我们选择两个:
更改异步方法的签名,以便它们返回Task或Task <>(在TypeScript中为Promise <>)
将异步方法的名称更改为以xxxAsync()结尾现在,当用异步组件替换低级同步组件时,这会影响应用程序的整个堆栈。由于异步/等待只有在“一路向上”使用时才会产生积极的影响,因此这意味着必须更改应用程序中每一层的签名和方法名称。
好的架构通常涉及放置抽象在每个层之间,这样高层就看不到其他层替换低层组件。在C#中,抽象采用接口的形式。如果我们引入了一个新的,低级的异步组件,则调用栈中的每个接口都需要修改或替换为一个新接口。在实现类中解决问题(异步或同步)的方式不再对调用方隐藏(抽象)。调用者必须知道它是同步还是异步。
不是异步/等待与“良好体系结构”原则相抵触的最佳实践吗?
这是否意味着每个接口(比如说IEnumerable,IDataAccessLayer)需要它们的异步对应对象(IAsyncEnumerable,IAsyncDataAccessLayer),以便在切换到异步依赖项时可以在堆栈中替换它们。假设每个方法都是异步的(返回Task <>或Promise <>)更简单,并且使方法在实际上不是异步的情况下同步异步调用,会更简单?这是将来的编程语言所期望的吗?
#1 楼
您的功能是什么颜色?您可能会对Bob Nystrom的《功能是什么颜色》感兴趣。
在本文中,他描述了一种虚构的语言,其中:每个功能都有一个颜色:蓝色或红色。
红色功能可以调用蓝色或红色功能,没问题。
蓝色功能只能调用蓝色功能。
虽然是虚构的
在C ++中非常常见:
在C ++中,“ const”方法只能在
this
上调用其他“ const”方法。 IO函数。在C#中,同步函数只能调用同步函数2。
您已经意识到,由于这些规则,红色函数倾向于在代码库中散布。您插入一个,然后一点一点地将其移植到整个代码库中。强烈建议用于任何编程语言/编译器爱好者。
2不太正确,因为您可以调用异步函数并阻塞直到返回,但是...语言/运行时限制。
例如M:N线程的语言(例如Erlang和Go)没有
async
函数:每个函数都可能是异步的,并且其“光纤”将被简单地挂起,交换C#采用1:1线程模型,因此决定在语言中实现同步,以避免意外阻塞线程。
在存在语言限制的情况下,编码准则必须适应。
评论
IO函数确实有扩展的趋势,但是通过勤奋,您可以将它们隔离到靠近代码入口点(在调用时在堆栈中)的函数。您可以通过让这些函数调用IO函数,然后让其他函数处理它们的输出并返回进一步IO所需的任何结果,来实现此目的。我发现这种风格使我的代码库更易于管理和使用。我想知道是否存在同步性。
– jpmc26
18/12/5在17:35
“ M:N”和“ 1:1”线程是什么意思?
–曼上尉
18/12/5在19:44
@CaptainMan:1:1线程意味着将一个应用程序线程映射到一个OS线程,在C,C ++,Java或C#等语言中就是这种情况。相比之下,M:N线程意味着将M个应用程序线程映射到N个OS线程;在Go的情况下,应用程序线程称为“ goroutine”,在Erlang的情况下,称为“ actor”,您可能还听说过它们是“绿色线程”或“纤维”。它们无需并行即可提供并发性。不幸的是,有关该主题的Wikipedia文章很少。
– Matthieu M.
18/12/5在20:53
这有点相关,但我也认为这种“功能颜色”的想法也适用于阻止用户输入的功能,例如模式对话框,消息框,某些形式的控制台I / O等,这些功能框架从一开始就具有。
– jrh
18/12/7在21:53
@MatthieuM。 C#的每个操作系统线程没有一个应用程序线程,也从未如此。当您与本机代码交互时,这非常明显,尤其是在MS SQL中运行时。当然,协作例程总是可能的(并且使用异步甚至更简单);实际上,这是构建响应式UI的一种非常常见的模式。和Erlang一样漂亮吗?不。但这与C仍然相去甚远:)
–罗安
18/12/9在7:30
#2 楼
没错,这里有一个矛盾,但这并不是“最佳实践”不好。这是因为异步功能所做的本质上不同于同步功能。它无需等待其依赖项(通常是某些IO)的结果,而是创建了一个由主事件循环处理的任务。这不是可以完全抽象地隐藏的区别。评论
答案就像这个IMO一样简单。同步过程和异步过程之间的区别不是实现细节,而是语义上不同的协定。
– Ant P
18/12/5在17:33
@AntP:我不同意这么简单。它以C#语言显示,但不以Go语言显示。因此,这不是异步过程的固有属性,这与如何在给定语言中建模异步过程有关。
– Matthieu M.
18/12/6在9:39
@MatthieuM。是的,但是您也可以在C#中使用异步方法来提供同步合同。唯一的区别是Go默认情况下是异步的,而C#默认情况下是同步的。异步为您提供了第二种编程模型-异步是抽象(它的实际作用取决于运行时,任务调度程序,同步上下文,等待程序的实现...)。
–罗安
18/12/9在7:33
#3 楼
就像您确定的那样,异步方法的行为不同于同步方法。在运行时,将异步调用转换为同步调用是微不足道的,但是不能说相反。因此,逻辑就变成了,为什么我们不对每个可能需要它的方法都使用异步方法,而让调用者根据需要“转换”为同步方法呢?从某种意义上说,这就像是拥有一个抛出异常的方法,另一个拥有“安全”且即使发生错误也不会抛出异常的方法。编码器在什么时候过度提供这些方法,否则这些方法可以相互转换?
这里有两种思路:一种是创建多个方法,每个方法都调用另一种可能的私有方法,从而允许为行为提供可选参数或较小的更改,例如异步。另一种方法是将接口方法最小化为基本内容,而由调用者自己进行必要的修改。同步和异步调用,以避免每个调用加倍。 Microsoft倾向于采用这种思想,并且按照惯例,为了与Microsoft所支持的样式保持一致,您也必须拥有Async版本,几乎与接口几乎总是以“ I”开头的方式相同。让我强调一下,从本质上讲,这并没有错,因为在项目中保持一致的样式比以“正确的方式”做到更好,并且从根本上更改添加到项目中的开发样式更好。
就是说,我倾向于第二所学校,即尽量减少接口方法。如果我认为某个方法可以异步方式调用,那么对我来说该方法是异步的。呼叫者可以决定在继续操作之前是否等待该任务完成。如果此接口是库的接口,则采用这种方式更合理,以最大程度减少您不赞成使用或调整的方法的数量。如果该接口供我的项目内部使用,我将为整个项目中的每个所需调用添加一个方法,以提供的参数,并且不添加“额外”方法,即使如此,前提是该方法的行为尚未涵盖通过现有方法。
但是,像该领域的许多事情一样,它在很大程度上是主观的。两种方法各有利弊。微软还开始约定在变量名的开头添加表示类型的字母,并使用“ m_”表示其为成员,从而导致诸如
m_pUser
之类的变量名。我的观点是,即使Microsoft也不是绝对可靠的,并且也会出错。 也就是说,如果您的项目遵循此异步约定,则建议您尊重它并继续使用该样式。而且只有给了自己的项目后,您才能以自己认为合适的最佳方式编写它。
评论
“在运行时,将异步调用转换为同步调用是微不足道的”,我不确定是否确实如此。在.NET中,使用.Wait()方法之类的方法可能会导致负面后果,而据我所知,在js中,这是完全不可能的。
– max630
18/12/5在10:00
@ max630我没有说没有并发问题要考虑,但是如果它最初是一个同步任务,很可能不会造成死锁。也就是说,琐碎并不意味着“双击此处即可转换为同步”。在js中,您将返回一个Promise实例,并在其上调用resolve。
–尼尔
18/12/5在10:25
是的,将异步转换回同步完全是个麻烦
–伊万
18/12/5在10:26
@Neil在javascript中,即使调用Promise.resolve(x)然后向其添加回调,这些回调也不会立即执行。
– NickL
18/12/5在16:54
@Neil如果接口公开了异步方法,则期望在Task上等待不会产生死锁,这不是一个好假设。接口显示它实际上在方法签名中是同步的,这比文档中可能在更高版本中更改的承诺要好得多。
–卡尔·沃尔什(Carl Walsh)
18/12/6在20:49
#4 楼
假设有一种方法可以使您以异步方式调用函数而无需更改其签名。那真是太酷了,没人会建议您更改它们的名称。级别对它们具有一些特定于其异步性质的结构。例如,
public class HTTPClient
{
public HTTPResponse GET()
{
//send data
while(!timedOut)
{
//check for response
if(response) {
this.GotResponse(response);
}
this.YouCanWait();
}
}
//tell calling code that they should watch for this event
public EventHander GotResponse
//indicate to calling code that they can go and do something else for a bit
public EventHander YouCanWait;
}
这是调用代码以异步方式运行代码所需的这两点信息,如
Task
和async
封装之类。有多种方法可以执行异步功能,
async Task
只是通过返回类型内置在编译器中的一种模式,因此您不必手动链接事件#5 楼
我将以不太C#ness的方式和更通用的方式解决要点:是否不异步/等待与“良好体系结构”原则相抵触的最佳实践?
我想说的是,这取决于您在API设计中所做的选择以及您向用户提供的内容。
如果您希望API的一个功能能够只是异步,所以很少有兴趣遵循命名约定。只是总是返回Task <> / Promise <> / Future <> / ...作为返回类型,它是自记录的。如果想要一个同步的答案,他仍然可以通过等待来完成,但是如果他总是这样做,这会有些重复。
但是,如果仅使您的API同步,那意味着如果用户希望它异步,则他必须自己管理它的异步部分。
这可以做很多额外的工作,但是,它还可以使用户更好地控制他允许多少个并发呼叫,放置超时,重试等。
在具有庞大API的大型系统中,将它们中的大多数实现为默认情况下的同步可能比独立管理API的每个部分更容易和更有效,特别是如果它们共享资源(文件系统,CPU,数据库,... )。
实际上,对于最复杂的部分,您可以完美地对API的同一部分进行两种实现,一种是同步处理方便的事情,一种异步依赖于同步处理一件事情,仅管理并发性,负载,超时和重试。
也许其他人可以分享他的经验,因为我缺乏此类系统的经验。
评论
@Miral在两种可能性中都使用了“从同步方法调用异步方法”。
– Adrian Wragg
18/12/6在11:37
@AdrianWragg所以我做到了;我的大脑一定有种族状况。我会解决的。
– Miral
18/12/7在2:13
相反。从sync方法调用异步方法很简单,但是从async方法调用sync方法是不可能的。 (而且,如果有人尝试执行后者,事情就会完全崩溃,这可能会导致死锁。)因此,如果必须选择一个,默认情况下异步是更好的选择。不幸的是,这也是较困难的选择,因为异步实现只能调用异步方法。
– Miral
18/12/7在2:13
(这当然意味着阻塞同步方法。您可以从异步方法中调用一些可以同步进行纯CPU限制的计算的方法-尽管您应该避免这样做,除非您知道自己处于工作环境中而不是UI上下文-但是阻止在锁,I / O或其他操作上等待空闲的调用是个坏主意。)
– Miral
18/12/7在2:17
评论
虽然这听起来像是一个很棒的讨论问题,但我认为这是基于观点的,因此无法在此处回答。@Euphoric:我认为这里解决的问题比C#指南要深,这只是一个事实的征兆,即将应用程序的某些部分更改为异步行为可能会对整个系统产生非本地影响。因此,我的直觉告诉我,基于技术事实,对此必须有一个没有质疑的答案。因此,我鼓励在座的每个人不要过早地结束这个问题,而要让我们等待将会给出什么样的答案(如果他们太自以为是,我们仍然可以投票赞成结束)。
@DocBrown我认为这里更深层的问题是“是否可以将系统的一部分从同步更改为异步,而不必依赖于系统的部分也进行更改?”我认为答案是明确的“不”。在那种情况下,我看不到“好的架构和分层的概念”在这里如何适用。
@Euphoric:听起来像是获得非意见解答的良好基础;-)
@Gherman:因为C#和许多语言一样,不能仅基于返回类型进行重载。您最终将获得与同步对象具有相同签名的异步方法(并非所有人都可以使用CancellationToken,并且可能希望提供默认值)。显然,删除现有的同步方法(并主动破坏所有代码)是一个不起眼的问题。