将同步代码包装到异步调用中

时间:2014-01-28 13:32:20

标签: c# asp.net asynchronous async-await c#-5.0

我在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);
    });
}

用法(“do do stuff”代码的一部分与服务调用同时运行):

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是描述我的案例的好文章,它没有描述并行调用的情况,拒绝包装同步调用的选项,但在我看来,我的情况恰好是这样做的机会。
提前感谢您的帮助和提示。

3 个答案:

答案 0 :(得分:87)

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

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

  

方法本身是对服务的同步调用,并且不可能覆盖实现。

如果您的“服务”是 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.InvokeParallel代码的优点是请求线程被用作并行线程之一,然后在线程上下文中继续执行(上下文切换比async示例更少):

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

我建议不要在ASP.NET上使用Parallel LINQ(PLINQ)。

答案 1 :(得分:0)

我发现以下代码可以将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()添加为被调用代码的第一行。

答案 2 :(得分:0)

这可能是最简单的通用方法

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