在.NET中执行Async/Await的两种错误方法

在.NET中执行异步/等待的两种错误方法

在应用开发中,我们为了提高应用程序的吞吐能力或者异步操作来减少耗时,通常会使用多线程来达到目的,而在C#语言中由于async/await必杀技的存在,大多会使用此来简化多线程操作,async/await的具体使用方式想必大家已烂熟于心,不再赘述,今天主要谈谈在我们经常所谓的async/await操作真的是正确的吗?你又真的用对他了吗?.

情景1:消除异步方法优势

请看下面代码示例

ServiceClient client = new ServiceClient();
ServiceRequest request = new ServiceRequest();
request.Id = newId;            
var responseTask = Task.Run(() => client.GetServicesAsync(request));
ServiceResponse response = await responseTask;

以上代码片段在一个异步方法中,此方法在另一个Task中返回一个Task!(Task.Run) 这是多余的。如果该方法已经返回Task,则我们不应该将其包装在另一个Task中。因此,在这种情况下,解决方案也非常简单,取消一切不必要的Task:

ServiceClient client = new ServiceClient();
ServiceRequest request = new ServiceRequest();
request.Id = newId;            
var responseTask = client.GetServicesAsync(request);
ServiceResponse response = await responseTask;

情景2:滥用Task.Run()

在工作中大多使用Task.Run的代码给人的印象是异步有助于提高性能。这是正确的,但仅是非常片面的。Async/Await的目的是帮助提高吞吐量。改善性能仅仅是副作用。

因此在工作中会发现各种奇奇怪怪的代码,例如以下为了配合外部异步方法,又由于内部各种原因没有实现异步方法,不得不用Task.Run来包裹同步方法而达到语法要求。

MyService client = new MyService();
var responseTask = Task.Run(() => client.GetData(request));
var response = await responseTask;

换句话说:使用Task.Run()包装了一个同步调用。

这里的问题是方法client.GetData()本身并不是异步方法,通过将异步包装器置于同步方法之上,我们正在做一个称为“async-over-sync异步超同步”的反模式,这在大多数情况下最终不是一个推荐的做法。不是因为它不起作用(而是起作用),而是因为它效率不高。

之所以如此,原因是很长的,而且涉及很多,如果感兴趣可在文章末尾找到Stephen作者相关对此问题的详细解释地址。总结一下,以上代码非常糟糕,因为实现异步的好处是通过在线程不执行任何操作(例如,等待服务响应)时“释放”线程来提高吞吐量。上面的示例确实释放了一个线程,它也立即消耗了另一个线程来执行任务包装的代码,并且该消耗的线程在等待服务响应时被阻塞。因此,我们没有提高吞吐量,只是将工作从一个线程转移到了另一个线程。而且在并发下,以上使用方式在工作中也极大的降低了系统性能!

解决方案可以简化为:不要对同步方法使用异步包装器!只需同步调用它们即可。在这种情况下,理论上的性能优势将被潜在的问题所抵消,这些潜在的问题在最坏的情况下可能包括死锁。

过度使用Task.Run()有很大安全隐患,尤其在你未搞懂你写了什么的时候,这种影响在复杂业务和超大并发下出问题非常难排查!在发现性能严重影响又找不到原因的时候,请排查出所有使用Task.Run的代码,确定是否是以上两种情况,解决他们可能就海阔天空了

摘要

在.NET或者.Netcore中使用Async/Await都是一项技巧,必须谨慎使用。在上面的示例中,开发团队试图使他们的应用程序性能更好,但最终由于对他们的代码过度使用Async/Await而使情况变的难以控制。

总之应该记住两件事:

不要将异步任务包装在另一个异步包装器Task.Run中。

不要在同步调用上使用异步包装器。

有很多方法可以修正使用异步/等待的ASP.NET代码。