我继承了一个使用MVC5和C#的大型Web应用程序。我们的一些控制器进行了几次慢速数据库调用,我想让它们异步,以便允许工作线程在等待数据库调用完成时为其他请求提供服务。我想用最少量的重构来做到这一点。说我有以下控制器
public string JsonData()
{
var a = this.servicelayer.getA();
var b = this.servicelayer.getB();
return SerializeObject(new {a, b});
}
我通过保持服务层不变并将控制器重写为
,使两个昂贵的调用a,b异步public async Task<string> JsonData()
{
var task1 = Task<something>.Run(() => this.servicelayer.getA());
var task2 = Task<somethingelse>.Run(() => this.servicelayer.getB());
await Task.WhenAll(task1, task2);
var a = await task1;
var b = await task2;
return SerializeObject(new {a, b});
}
上面的代码运行没有任何问题,但我不能告诉使用Visual Studio,如果工作线程现在可以为其他请求提供服务,或者如果在asp.net控制器中使用Task.Run()则不会。做我认为它做的事情。任何人都可以评论我的代码的正确性,以及它是否可以以任何方式改进?另外,我读到在控制器中使用async会产生额外的开销,应该只用于长时间运行的代码。我可以用来确定控制器是否需要异步的最低标准是什么?我知道每个用例都不同,但想知道是否有一个基线我可以用作起点。 2个数据库通话?什么超过2秒返回?
答案 0 :(得分:6)
指南是,只要有I / O,就应该使用异步。即,一个数据库。与任何类型的I / O相比,开销微乎其微。
也就是说,通过Task.Run
阻塞线程池线程就是我所谓的“假异步”。这正是你 想要在ASP.NET上做的事情。
相反,从“最低级别”代码开始,使其真正异步。例如,EF6支持异步数据库查询。然后让async
代码从那里自然地增长到你的控制器。
答案 1 :(得分:2)
新代码的唯一改进是它同时运行A和B,而不是一次运行一个。在这段代码中实际上没有真正的异步。
当你使用Task.Run
时,你正在卸载要在另一个线程上完成的工作,所以基本上你在等待两个任务时启动2个线程并释放当前线程(每个线程完全同步运行)
这意味着操作将更快完成(因为并行性),但将使用两次线程,因此可扩展性较差。
您做想要做的是确保您的所有操作都是真正异步的。这意味着拥有servicelayer.getAAsync()
和servicelayer.getBAsync()
,这样您就可以在处理IO时真正释放线程:
public async Task<string> JsonData()
{
return SerializeObject(new {await servicelayer.getAAsync(), await servicelayer.getBAsync()});
}
如果您无法确保实际的IO操作真的是异步,那么保留旧代码会更好。
有关避免Task.Run
:Task.Run Etiquette Examples: Don't Use Task.Run in the Implementation