在 WPF 事件处理器中使用 Task.Run 的方式,是否适用于 ASP.NET MVC 控制器呢,答案是否定的。
class MyService
{
public int CalculateMandelbrot()
{
// CPU-bound 操作
for (int i = 0; i != 10000000; ++i)
{
// 一些计算逻辑
}
return 100;
}
}
public class MandelbrotController: Controller
{
public ActionResult Index()
{
var result = myService.CalculateMandelbrot();
return View(result);
}
}
到现在为止还挺好。当请求进来时,控制器使用服务(同步)计算视图数据。在该计算期间内,都使用单个 ASP.NET 请求线程。
class MyService
{
public int CalculateMandelbrot()
{
// CPU-bound 操作
for (int i = 0; i != 10000000; ++i)
{
// 一些计算逻辑
}
return 100;
}
}
public class MandelbrotController: Controller
{
public async Task<ActionResult> IndexAsync()
{
var result = await Task.Run(() => { myService.CalculateMandelbrotAsync(); });
return View(result);
}
}
当我们进行测试时,它可以工作!不幸的是,在ASP.NET MVC 控制器中使用 Task.Run 异步包装器会引入了性能问题。
使用原始(同步)代码,从头到尾只使用一个线程来处理请求。这是一个高度优化的 ASP.NET 方案。使用 Task.Run 异步包装器,而不是单个请求线程,会发生以下情况:
这可以正常工作,但它根本没有效率,因为存在线程切换和线程回收过程。
只要你在 ASP.NET MVC 中使用带 await 的 Task.Run, 至少会引入 3 个效率问题:
如果您多次调用 Task.Run,则性能问题会更加复杂。在繁忙的服务器上,这种实现会扼杀可伸缩性。
这就是为什么 ASP.NET 的原则之一是避免使用线程池线程(当然除了 ASP.NET 给你的请求线程)。更重要的是,这意味着 ASP.NET 应用程序应该避免使用 Task.Run。
现在我,们知道该实现存在什么问题。显而易见的事实是:如果服务 API 是 CPU-bound 方法,则 ASP.NET 更喜欢同步方法;如果服务 API 是 I/O-bound 方法,也许 ASP.NET 更喜欢异步方法。对于其他场景也是如此:控制台应用程序、桌面应用程序中的后台线程等。事实上,我们真正需要异步计算的唯一地方就是我们从 UI 线程调用它的时候。