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


95

我在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方法已经提供了方法,但我的情况并非如此。这里是描述我的情况的不错的文章,它没有描述并行调用的情况,但拒绝了包装同步调用的选项,但是在我看来,我的情况正是这样做的机会。
在此先感谢您的帮助和提示。

Answers:


116

区分两种不同类型的并发很重要。异步并发是指在运行中有多个异步操作时(并且由于每个操作都是异步的,因此它们实际上都没有使用线程)。并行并发是指您有多个线程分别执行单独的操作。

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

该方法本身是对服务的同步调用,无法覆盖实现。

如果您的“服务”是Web服务或其他受I / O绑定的内容,那么最好的解决方案是为其编写异步API。

我将假设您的“服务”是一个CPU约束的操作,必须与Web服务器在同一台计算机上执行。

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

我需要执行更快的请求。

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

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

在这种情况下,您将需要在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.Invoke。该Parallel代码的优势在于,请求线程被用作并行线程之一,然后在线程上下文中恢复执行(上下文切换比async示例少):

private void MakeRequest()
{
  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));
}

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


1
非常感谢您提供详细的答案。我还要澄清一件事。当我使用开始执行Task.Run内容时,内容在另一个线程中运行,该线程取自线程池。然后,根本不需要将阻塞调用(例如,对服务的防雷调用)包装到Runs中,因为它们总是每个消耗一个线程,而该线程将在方法执行期间被阻塞。在这种情况下async-await,我剩下的唯一好处就是一次执行多个操作。请,如果我错了,请纠正我。
伊德尔2014年

2
是的,从Run(或Parallel)获得的唯一好处是并发性。这些操作仍然各自阻塞线程。由于您说该服务是Web服务,因此不建议使用RunParallel;。而是为该服务编写异步API。
Stephen Cleary 2014年

3
@StephenCleary。您的意思是“ ...并且由于每个操作都是异步的,因此它们实际上都没有使用线程...”,谢谢!
alltej

3
@alltej:对于真正的异步代码,没有thread
史蒂芬·克利西

4
@JoshuaFrank:不直接。根据定义,同步API背后的代码必须阻止调用线程。您可以使用像这样的异步API BeginGetResponse并启动其中一些,然后(同步)阻止,直到所有这些都完成,但是这种方法很棘手-请参阅blog.stephencleary.com/2012/07/dont-block-on -async-code.htmlmsdn.microsoft.com/en-us/magazine/mt238404.aspxasync如果可能的话,通常更容易,更清洁地采用所有方式。
Stephen Cleary

1

我发现以下代码可以将Task转换为始终异步运行

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)
{
    await Task.Yield();
    return await func();
}

我已经以以下方式使用了它

await ForceAsync(() => AsyncTaskWithNoAwaits())

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.