编写一个设计良好的异步/非同步API
本文关键字:异步 API 非同步 一个 | 更新日期: 2023-09-27 17:58:59
我面临着设计用于执行网络I/O(用于可重用库)的方法的问题。我读过这个问题
API设计中的c#5等待/异步模式
还有其他更接近我的问题。
所以,问题是,如果我想同时提供异步和非异步方法,我必须如何设计这些方法?
例如,为了公开一个方法的非异步版本,我需要做一些类似的事情
public void DoSomething() {
DoSomethingAsync(CancellationToken.None).Wait();
}
我觉得这不是一个好的设计。我想要一个关于如何定义私有方法的建议(例如),这些方法可以封装在公共方法中以提供两个版本。
如果您想要最可维护的选项,只需提供async
API,该API在实现时不会进行任何阻塞调用或使用任何线程池线程。
如果您真的想同时拥有async
和同步API,那么您将遇到可维护性问题。您确实需要实现两次:一次是async
,一次是同步的。这两种方法看起来几乎相同,因此最初的实现很容易,但最终会有两种几乎相同的方法,因此维护是有问题的。
特别地,没有一种好的简单的方法来仅仅制作async
或同步";包装器";。Stephen Toub有关于这个主题的最佳信息:
- 我应该公开同步方法的异步包装器吗
- 我应该公开异步方法的同步包装器吗
(两个问题的简短回答都是"否")
然而,如果你想避免重复的实现,你可以使用一些技巧;最好的方法通常是布尔参数破解。
我同意Marc和Stephen(Cleary)的观点。
(顺便说一句,我开始写这篇文章作为对斯蒂芬答案的评论,但结果太长了;让我知道把它作为答案写是否可以,本着"提供最好的答案"的精神,可以随意从中提取一些内容并添加到斯蒂芬的答案中)。
这真的"取决于":正如Marc所说,了解DoSomethingAsync是如何异步的很重要。我们都同意,让"sync"方法调用"async"方法和"wait"是没有意义的:这可以在用户代码中完成。拥有单独方法的唯一优点是可以获得实际的性能增益,实现方式在本质上是不同的,并根据同步场景进行定制。如果"async"方法正在创建一个线程(或从线程池中获取线程),则情况尤其如此:您最终得到的东西在下面使用了两个"控制流",而其同步外观的"promise"将在调用方的上下文中执行。这甚至可能存在并发问题,具体取决于实现。
此外,在其他情况下,如OP提到的密集型I/O,可能值得采用两种不同的实现方式。大多数操作系统(当然是Windows)都有针对这两种场景定制的不同I/O机制:例如,异步执行和I/O操作从操作系统级机制(如I/O完成端口)中获得了巨大优势,这些机制在内核中增加了一点开销(不重要,但不是空的)(毕竟,它们必须进行记账、调度等),以及用于同步操作的更直接的实现。代码的复杂性也有很大的差异,尤其是在执行/协调多个操作的功能中。
我会做的是:
- 有一些典型用法和场景的示例/测试
- 查看使用了哪种API变体、何处和度量。还可以测量"纯同步"变体和"同步"之间的性能差异。(不适用于整个API,但适用于选定的少数典型案例)
- 根据测量结果,决定增加的成本是否值得
这主要是因为两个目标在某种程度上是相互对比的。如果您想要可维护的代码,那么显而易见的选择是按照async/wait(或相反的方式)实现同步(或者,更好的是,只提供async变体,让用户"等待");如果你想要性能,你应该以不同的方式实现这两个功能,以利用不同的底层机制(从框架或操作系统)。我认为,从单元测试的角度来看,如何实际实现API不应该有什么不同。
我遇到了同样的问题,但使用关于异步方法的两个简单事实,我设法在效率和可维护性之间找到了折衷方案:
- 不执行任何等待的异步方法是同步的
- 只等待同步方法的异步方法是同步的
这最好在示例中显示:
//Simple synchronous methods that starts third party component, waits for a second and gets result.
public ThirdPartyResult Execute(ThirdPartyOptions options)
{
ThirdPartyComponent.Start(options);
System.Threading.Thread.Sleep(1000);
return ThirdPartyComponent.GetResult();
}
为了提供这种方法的可维护同步/异步版本,它被分为三层:
//Lower level - parts that work differently for sync/async version.
//When isAsync is false there are no await operators and method is running synchronously.
private static async Task Wait(bool isAsync, int milliseconds)
{
if (isAsync)
{
await Task.Delay(milliseconds);
}
else
{
System.Threading.Thread.Sleep(milliseconds);
}
}
//Middle level - the main algorithm.
//When isAsync is false the only awaited method is running synchronously,
//so the whole algorithm is running synchronously.
private async Task<ThirdPartyResult> Execute(bool isAsync, ThirdPartyOptions options)
{
ThirdPartyComponent.Start(options);
await Wait(isAsync, 1000);
return ThirdPartyComponent.GetResult();
}
//Upper level - public synchronous API.
//Internal method runs synchronously and will be already finished when Result property is accessed.
public ThirdPartyResult ExecuteSync(ThirdPartyOptions options)
{
return Execute(false, options).Result;
}
//Upper level - public asynchronous API.
public async Task<ThirdPartyResult> ExecuteAsync(ThirdPartyOptions options)
{
return await Execute(true, options);
}
这里的主要优点是,最有可能更改的中级算法只实现一次,因此开发人员不必维护两段几乎相同的代码。