【问题标题】:Write a well designed async / non-async API编写一个设计良好的异步/非异步 API
【发布时间】:2013-02-14 08:17:40
【问题描述】:

我面临着设计执行网络 I/O 的方法(对于可重用库)的问题。我读过这个问题

c# 5 await/async pattern in API design

还有其他更接近我的问题。

所以,问题是,如果我想同时提供 异步和非异步 方法,我该如何设计这些方法?

例如,要公开方法的非异步版本,我需要做类似的事情

public void DoSomething() {
  DoSomethingAsync(CancellationToken.None).Wait();
}

我觉得这不是一个伟大的设计。我想要一个建议(例如)关于如何定义可以包装在公共方法中以提供两个版本的私有方法。

【问题讨论】:

  • 重要问题:DoSomethingAsync 的异步部分是如何实现的?这很重要,因为如果这是创建 Task 和工作线程,那么答案可能与使用外部事件 API 的情况大不相同。具体来说,如果“async”方法正在占用一个线程,那么让“sync”方法调用“async”方法和“wait”是没有意义的:最好直接完成工作。但是,这对于“异步”的其他含义有所改变(并非所有“异步”都表示“线程”)
  • @MarcGravell,我试图解释得更好(这并不简单,因为英语不是我的主要语言)。我有执行密集 I/O 的“操作 XYZ”。我想用两种方法在库中公开它:一种使用异步模式几乎立即返回(如 MS Task-based Asynchronous Pattern doc 中所述),另一种执行同步。什么是更高效 / 可维护 / 启用单元测试的方法。使用公共异步包装器和另一个同步包装器公开的私有方法,例如?如果是,这些方法是怎样的?
  • “高效”和“可维护”不是一回事,而且经常相互矛盾。再说一遍:这完全取决于您的“异步”代码当前的工作方式。如果那是您只是在单独的线程上运行的程序/线性代码,那么坦率地说,通过“异步”公开它是没有意义的。如果该代码是完全异步的,那么如果您希望“高效”作为目标,那么最好有 2 个完全独立的实现。抱歉,这个问题非常是特定于上下文的,你没有给出太多上下文。
  • @MarcGravell,是的,老实说,高效+可维护这一对是一种天真的挑衅,它可以刺激和扩展对话,而不会以偏离主题的评论结束(我希望如此)。我很难回答你关于如何实现DoSomethingAsync 的问题,因为我实际上只有一个同步存根,我愿意从头开始重写所有内容。我怀疑这完全取决于具体操作的复杂性,不幸的是每个案例都必须单独评估,并且没有共同的模式可以依赖。
  • 因燃烧我的大脑而被投票

标签: c# asynchronous


【解决方案1】:

如果您想要最可维护的选项,只需提供async API,该 API 的实现无需进行任何阻塞调用或使用任何线程池线程。

如果您真的想同时拥有async 和同步API,那么您将遇到可维护性问题。你真的需要实现它两次:一次async 和一次同步。这两种方法看起来几乎相同,因此初始实现很容易,但您最终会得到两个几乎相同的独立方法,因此维护存在问题。

特别是,没有一种简单的方法来制作async 或同步“包装器”。 Stephen Toub 提供了有关该主题的最佳信息:

  1. Should I expose asynchronous wrappers for synchronous methods?
  2. Should I expose synchronous wrappers for asynchronous methods?

(这两个问题的简短回答都是“否”)

但是,如果您想避免重复实现,可以使用一些技巧;最好的通常是boolean argument hack

【讨论】:

  • {+1} 这是我需要听到的!与此同时,我最终重写了真正异步的方法,不再关心同步版本。毕竟,如果消费者愿意,他们可以从他们的代码调用 DoSomethingAsync.Wait()。我担心 Mono 兼容性,但配置文件 3.0 支持 async/await 关键字,所以问题不存在。顺便说一句,很棒的文章。
  • @jay 我会比 +1 更进一步;我目前正在与 Stephen Toub 进行一次会议,这个答案是对一个非常复杂的问题的理想答案。所以我会全力以赴 +500。
  • @MarcGravell:谢谢!他们不会碰巧录制斯蒂芬的会议,是吗? :) 我几乎从来没有去过这个国家的那一边......
  • @Stephen 我不这么认为,但这些都不是保密协议,所以他们(Stephen Toub 和 Lucian Wischik)(有人要求)同意两者:提供幻灯片,并写一些博客涵盖的内容(尽管很多内容已经写在博客上)
  • 我想补充一件事。指出在设计新 API 时不应将 sync 方法包装在 async 方法中(或相反)。我认为可接受的折衷方案可能是(1)将代码分解为小的可重用类型,(2)创建两个版本以减少重复但实际上提供两种不同的实现。如果有意义并且确实有必要,我想应该这样做。没有特定的模式,但必须单独评估每种情况。当然,之前(对我而言)所说的一切都得到了证实。
【解决方案2】:

我同意 Marc 和 Stephen (Clary) 的观点。

(顺便说一句,我开始写这篇文章作为对斯蒂芬答案的评论,但结果太长了;让我知道是否可以将其写为答案,并随时从并将其添加到斯蒂芬的答案中,本着“提供一个最佳答案”的精神)。

这真的“取决于”:就像 Marc 所说,了解 DoSomethingAsync 是如何异步的很重要。我们都同意让“sync”方法调用“async”方法和“wait”是没有意义的:这可以在用户代码中完成。拥有单独方法的唯一优点是获得实际的性能提升,并拥有一个在底层不同且针对同步场景量身定制的实现。如果“异步”方法正在创建线程(或从线程池中获取),则尤其如此:您最终会得到下面使用两个“控制流”的东西,而“有希望”的同步看起来会在调用者的上下文。这甚至可能存在并发问题,具体取决于实现。

此外,在其他情况下,例如 OP 提到的密集 I/O,可能值得拥有两种不同的实现。大多数操作系统(当然是 Windows)对于 I/O 具有针对这两种场景量身定制的不同机制:例如,异步执行和 I/O 操作从操作系统级机制(如 I/O 完成端口)中获得了很大优势,这增加了一点内核中的开销(不显着,但不为空)(毕竟它们必须做簿记、调度等),以及更直接的同步操作实现。 代码复杂度也有很大差异,尤其是在完成/协调多个操作的函数中。

我会做的是:

  • 有一些典型用法和场景的示例/测试
  • 查看使用的 API 变体、位置和测量。还测量“纯同步”变体和“同步”之间的性能差异。 (不是针对整个 API,只针对选择的几个典型案例)
  • 根据衡量结果,决定增加的成本是否值得。

这主要是因为两个目标在某种程度上相互对比。如果您想要可维护的代码,显而易见的选择是根据异步/等待(或相反)实现同步(或者,更好的是,只提供异步变体并让用户“等待”);如果您想要性能,您应该以不同的方式实现这两个功能,以利用不同的底层机制(来自框架或来自操作系统)。我认为从单元测试的角度来看,您实际实现 API 的方式不应该有所不同。

【讨论】:

  • 异步编程有一点点开销,但这都在用户模式方面。 Windows 内核是 100% 异步的;在设备驱动程序级别没有同步调用。
  • {+1} @dema80 您的回答广告有趣的操作系统注意事项。我还想听听 *nix/Mono (用于密集 I/O)中低级别发生的情况。可以添加的另一件事是相对于我的上一条评论。代码可以被分解然后重用以获得两个真正不同的实现吗?没有包装,正如你所说的解决两个版本。这不是一整天的编程,而是一个框架设计问题。毕竟 BCL 有很多 API 并排同步/异步版本。
  • @StephenCleary 当然,没有现代内核会同步阻塞:在低级别之后,I/O 是一个异步作业(中断、IPC 等)。这不是我的意思,我只是指出同步函数的实现可能更直接:我不确定在用户模式下发生的所有事情:我认为使用同步 I/O 线程构建 IRP,将其存储在设备堆栈中,并在内核中等待 IRP 完成。对于异步 I/O,APC 或额外的队列是必要的。但后来,我在 Windows 2000 时代从事这方面的工作,情况可能发生了变化!
  • @jay 在谈论 Windows 内部时我非常自信,但我不是在谈论 Unix(请记住,低级的东西在不同的风格之间有很大的不同)。至于您的另一个问题,我也想知道如何增加重用性;至于 BCL,我的猜测是它的底层只是调用了等效的 Win32 函数(毕竟,win32 API 已经实现了异步/同步 I/O,而后者又使用了 NT 内核中实现的不同函数)。跨度>
  • @dema80,同意 win32 API 的观点,正如你所说,在这种情况下研究托管代码重用可能会很有趣。
【解决方案3】:

我遇到了同样的问题,但使用关于异步方法的两个简单事实设法在效率和可维护性之间找到折衷:

  • 不执行任何await的异步方法是同步的;
  • 只等待同步方法的异步方法是同步的。

最好在示例中显示:

//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);
}

这里的主要优点是最有可能更改的中间层算法只实现一次,因此开发人员不必维护两段几乎相同的代码。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-12-11
    • 1970-01-01
    • 2020-11-10
    • 2013-02-03
    • 2018-01-26
    相关资源
    最近更新 更多