57

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

c# 5 API 设计中的等待/异步模式

以及其他更接近我的问题的人。

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

例如,要公开方法的非异步版本,我需要执行类似的操作

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

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

4

3 回答 3

71

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

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

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

  1. 我应该为同步方法公开异步包装器吗?
  2. 我应该为异步方法公开同步包装器吗?

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

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

于 2013-02-14T13:54:32.017 回答
3

我同意马克和斯蒂芬(克利里)的观点。

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

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

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

我会做的是:

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

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

于 2013-02-21T10:42:25.627 回答
1

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

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

最好在示例中显示:

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

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

于 2018-11-06T18:17:01.923 回答