使控制器异步化的最简单方法

4

我接手了一个使用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会增加额外开销,应仅用于长时间运行的代码。我可以使用哪些最低标准来决定控制器是否需要async呢?我明白每个使用场景都不同,但想知道是否有一个基准线可以作为起点。2个数据库调用?超过2秒返回?
2个回答

6
指南是,当您有I/O时应使用异步。例如数据库。与任何类型的I/O相比,开销微不足道。
话虽如此,通过Task.Run阻塞线程池线程是我所谓的“假异步”。这正是您不想在ASP.NET上执行的操作。
相反,请从您的“最低级”代码开始,使其真正异步化。例如,EF6支持异步数据库查询。然后让async代码自然而然地从那里向您的控制器发展。

2
新代码唯一的改进就是可以同时运行A和B,而不是一个接一个地运行。实际上,这段代码中并没有真正的异步操作。
当你使用Task.Run时,你正在将工作转移到另一个线程上完成,所以基本上你启动了两个线程并释放了当前线程,同时等待两个任务(它们每个都完全同步运行)。
这意味着操作将更快地完成(由于并行性),但将使用两倍的线程,因此可扩展性会降低。
您要做的是确保所有操作都是真正的异步。这意味着需要有一个 servicelayer.getAAsync()servicelayer.getBAsync(),这样在处理 I/O 时可以真正释放线程:
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

2
@l3arnon - 我的问题得到了两个有用的答案,我看到你发布的链接实际上是由Stephen Cleary撰写的。最终我选择了这个答案,因为Stack Overflow只允许你选择一个答案,而你的回答是第一个。非常感谢所有回答我的人。 - user1625066

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接