将同步代码包装成异步调用

Posted

技术标签:

【中文标题】将同步代码包装成异步调用【英文标题】:Wrapping synchronous code into asynchronous call 【发布时间】:2014-02-19 19:59:30 【问题描述】:

我在 ASP.NET 应用程序中有一个方法,需要花费大量时间才能完成。在一个用户请求期间,对该方法的调用最多可能发生 3 次,具体取决于用户提供的缓存状态和参数。每次通话大约需要 1-2 秒才能完成。方法本身是对服务的同步调用,不可能重写实现。 所以对服务的同步调用如下所示:

public OutputModel Calculate(InputModel input)

    // do some stuff
    return Service.LongRunningCall(input);

并且方法的用法是(注意,方法的调用可能不止一次):

private void MakeRequest()

    // a lot of other stuff: preparing requests, sending/processing other requests, etc.
    var myOutput = Calculate(myInput);
    // stuff again

我尝试从我这边更改实现以提供此方法的同步工作,这就是我到目前为止的结果。

public async Task<OutputModel> CalculateAsync(InputModel input)

    return await Task.Run(() =>
    
        return Calculate(input);
    );

用法(部分“做其他事情”代码与调用服务同时运行):

private async Task MakeRequest()

    // do some stuff
    var task = CalculateAsync(myInput);
    // do other stuff
    var myOutput = await task;
    // some more stuff

我的问题如下。我是否使用正确的方法来加快 ASP.NET 应用程序中的执行速度,或者我是否在尝试异步运行同步代码做不必要的工作?谁能解释为什么第二种方法在 ASP.NET 中不是一个选项(如果它真的不是)?另外,如果这种方法适用,如果这是我们目前可能执行的唯一调用,我是否需要异步调用这种方法(我有这种情况,在等待完成时没有其他事情要做)? 网上关于这个主题的大多数文章都涵盖了使用 async-await 方法和代码,它已经提供了 awaitable 方法,但这不是我的情况。 Here 是描述我的案例的好文章,它没有描述并行调用的情况,拒绝包装同步调用的选项,但我认为我的情况正是这样做的机会。 提前感谢您的帮助和提示。

【问题讨论】:

【参考方案1】:

区分两种不同类型的并发非常重要。 异步并发是当你有多个异步操作在运行时(因为每个操作都是异步的,它们实际上都没有使用一个线程)。 并行并发是指您有多个线程,每个线程都执行单独的操作。

首先要做的是重新评估这个假设:

方法本身是对服务的同步调用,不可能重写实现。

如果您的“服务”是 web 服务或其他任何受 I/O 限制的服务,那么最好的解决方案是为其编写异步 API。

我将继续假设您的“服务”是一个 CPU 密集型操作,必须在与 Web 服务器相同的机器上执行。

如果是这样,那么接下来要评估的是另一个假设:

我需要更快地执行请求。

您确定这是您需要做的吗?您是否可以进行任何前端更改 - 例如,启动请求并允许用户在处理时执行其他工作?

我将继续假设是的,您确实需要使单个请求执行得更快。

在这种情况下,您需要在网络服务器上执行并行代码。通常绝对不建议这样做,因为并行代码将使用 ASP.NET 可能需要处理其他请求的线程,并且通过删除/添加线程,它将关闭 ASP.NET 线程池试探法。因此,此决定确实会对您的整个服务器产生影响。

当您在 ASP.NET 上使用并行代码时,您决定真正限制 Web 应用程序的可伸缩性。您还可能会看到相当多的线程流失,尤其是在您的请求是突发性的情况下。如果您知道同时用户的数量会非常低(即,不是公共服务器),我建议仅在 ASP.NET 上使用并行代码。

所以,如果您走到这一步,并且确定要在 ASP.NET 上进行并行处理,那么您有几个选择。

其中一种更简单的方法是使用Task.Run,与您现有的代码非常相似。但是,我不建议实现 CalculateAsync 方法,因为这意味着处理是异步的(事实并非如此)。相反,在调用时使用Task.Run

private async Task MakeRequest()

  // do some stuff
  var task = Task.Run(() => Calculate(myInput));
  // do other stuff
  var myOutput = await task;
  // some more stuff

或者,如果它适用于您的代码,您可以使用Parallel 类型,即Parallel.ForParallel.ForEachParallel.InvokeParallel 代码的优点是请求线程被用作并行线程之一,然后在线程上下文中恢复执行(上下文切换比 async 示例少):

private void MakeRequest()

  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));

我完全不建议在 ASP.NET 上使用并行 LINQ (PLINQ)。

【讨论】:

非常感谢您的详细回答。我还想澄清一件事。当我使用Task.Run 开始执行时,内容在另一个线程中运行,该线程取自线程池。然后根本不需要将阻塞调用(例如我的对服务的调用)包装到Runs 中,因为它们将始终每个消耗一个线程,该线程将在方法执行期间被阻塞。在这种情况下,在我的情况下,async-await 留下的唯一好处是一次执行多个操作。如果我错了,请纠正我。 是的,您从Run(或Parallel)获得的唯一好处是并发性。这些操作仍然每个阻塞一个线程。既然你说服务是web服务,那么我不推荐使用Run或者Parallel;相反,为服务编写一个异步 API。 @StephenCleary。 “......因为每个操作都是异步的,它们实际上都没有使用线程......”谢谢! @alltej:对于真正的异步代码,there is no thread。 @JoshuaFrank:不直接。根据定义,同步 API 后面的代码必须阻塞调用线程。您可以使用像 BeginGetResponse 这样的异步 API 并启动其中几个,然后(同步)阻塞直到所有这些都完成,但这种方法很棘手 - 请参阅 blog.stephencleary.com/2012/07/dont-block-on-async-code.html 和 @ 987654323@ 。如果可能的话,一直采用async 通常更容易、更简洁。【参考方案2】:

我发现下面的代码可以将一个任务转换为始终异步运行

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)

    await Task.Yield();
    return await func();

我已经以下列方式使用它

await ForceAsync(() => AsyncTaskWithNoAwaits())

这将异步执行任何Task,因此您可以将它们组合在WhenAll、WhenAny场景和其他用途中。

您也可以简单地将 Task.Yield() 添加为调用代码的第一行。

【讨论】:

【参考方案3】:

这可能是你的情况下最简单的通用方法

return await new Task(
    new Action(
        delegate ()  
          // put your synchronous code here
        
    )
);

【讨论】:

不错,但为什么这个 3 级嵌套答案比 2014 年的答案更简单:var task = Task.Run(() =&gt; Calculate(myInput));

以上是关于将同步代码包装成异步调用的主要内容,如果未能解决你的问题,请参考以下文章

将同步调用转成异步调用?

同步调用异步 Javascript 函数

将非异步方法(进行网络调用)包装到异步中

使用async和await将同步方法包装成异步方法

ES6标准入门 摘要 (异步遍历器)

获取 异步执行调用的结果