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

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

I tried to change the implementation from my side to provide simultaneous work of this method, and here is what I came to so far.

public async Task<OutputModel> CalculateAsync(InputModel input)
{
return await Task.Run(() =>
{
return Calculate(input);
});
}

用法(“ do other 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方法,但是这不是我的情况。给你是一篇很好的文章,描述了我的情况,它没有描述并行调用的情况,拒绝了打包同步调用的选项,但是在我看来,我的情况正是这样做的机会。
Thanks in advance for help and tips.

101012 次浏览

It's important to make a distinction between two different types of concurrency. 异步的 concurrency is when you have multiple asynchronous operations in flight (and since each operation is asynchronous, none of them are actually using a 线). 平行 concurrency is when you have multiple threads each doing a separate operation.

要做的第一件事是重新评估这个假设:

The method itself is synchronous call to the service and there is no possibility to override the implementation.

如果您的“服务”是 服务或任何其他 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 上使用并行 LINQ (PLINQ)。

I found that the following code can convert a Task to always run asynchronously

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

and I have used it in the following manner

await ForceAsync(() => AsyncTaskWithNoAwaits())

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

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

this is probably the easiest generic way in your case

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