我该使用Task.Run来包装慢速调用吗?

4
我有一个ASP MVC调用,由于需要进行异步调用,因此它是异步的。我注意到有几个慢同步调用(例如数据库访问)。该方法需要所有返回的数据都可用才能继续。
我考虑使用Task.Run将同步调用包装起来,并等待它们全部完成。
将慢同步调用包装起来是否有意义?如果只有同步调用会怎样?
3个回答

6

除非您预计要同时服务的客户端请求数量非常低,否则不要在服务器端代码中使用Task.Run并行化工作。否则,您可能会加速单个请求的处理,但当有许多用户时,这将损害 Web 应用程序的可扩展性。


使用async/await的目的不就是允许保存当前线程而不是等待处理请求吗?如果需要并行任务,那么应该使用TPL,但使用TAP应该有助于可扩展性而不是阻碍它。 - Ryan Peters
1
@RyanPeters,确实如此,但在服务器端上下文中,您通常仅针对自然异步API使用TAP(请检查Stephen Cleary的"There Is No Thread")。这样,HTTP请求的初始线程被释放到池中,然后可以为另一个HTTP请求提供服务。但是,如果您使用Task.Run,则不会释放线程。至于包装单个同步调用,根本没有意义-您可能会在当前线程上执行它。至于并行包装多个调用-您会损害可扩展性。 - noseratio - open to work
1
对于客户端 UI 应用程序,情况可能会有所不同。在这种情况下,您不关心可扩展性,而是关心保持 UI 线程的响应能力。 - noseratio - open to work
@Noseratio - 你只是在消耗和阻塞线程池线程,而不是占用额外的CPU资源和内存(线程池已经分配,所以它的堆栈已经分配)。虽然这可能确实会影响可扩展性,但它是否成为问题取决于许多因素。我同意,从纯洁性的角度来看,如果可能的话最好避免它。 - Erik Funkenbusch
@ErikFunkenbusch,没错,但是仅仅使用(Task.Run)消耗并阻塞池线程可能会对可伸缩性产生非常严重的影响。还有臭名昭著的ThreadPool口吃行为需要记在心里,这里有一篇相关帖子附有一些研究资料。毕竟,为什么要通过使用Task.Run分叉阻塞调用来加速单个客户端的请求处理,而让其他客户端因没有空闲池线程而等待呢? - noseratio - open to work
是的,正如我所说的,这取决于许多因素,无论是否会成为一个问题。并行化任务可以对最终用户的性能产生显著影响,但必须与对系统的潜在影响进行平衡。许多因素可能导致可扩展性问题,然而我们仍然使用它们,只要你在做出权衡时有意识地选择。 - Erik Funkenbusch

3

试图通过启动线程来“加速”同步工作的尝试并不能很好地扩展@Noseratio。

需要记住的一件重要事情是,在ASP.NET中使用Task.Run非常危险,因为运行时不知道您排队等待完成的工作,IIS可能会定期尝试回收您的应用程序,这将导致工作意外中止。

如果您正在使用.NET Framework 4.5.2,则可以通过HostingEnvironment.QueueBackgroundWorkItem解决此问题。您可以在ASP.NET上的Fire and Forget中了解更多信息。如果没有,请阅读从ASP.NET请求中提前返回以获取自定义实现。这两篇优秀文章都是由@StephanCleary撰写的。


1
我认为作者在这里并没有谈论早期返回。你正在谈论fire and forget,或者是async void方法,这可能会导致你所说的问题。在这种情况下,他需要工作的结果,因此他必须等待它们完成,工作进程不会被回收。因此,使用Task.Run这种方式并没有什么“危险”,尽管正如@Noseratio所提到的那样,它对可扩展性不是很好。 - Erik Funkenbusch
他正在等待那些任务,没有理由为什么IIS不会尝试回收即使他正在“等待”。我不是在谈论异步void或fire and forget,我是在谈论ASP.NET中通常使用的Task.Run。 - Yuval Itzchakov
1
如果主要的操作线程尚未完成并正在等待,除非超时,否则它不会被回收。如果您不是在谈论Fire and Forget,为什么要引用与此相关的两篇文章呢? - Erik Funkenbusch
我认为@ErikFunkenbusch在这里有一点道理。也就是说,当执行点在async Web API方法内部第一次遇到await时,主要的操作线程(HTTP请求最初着陆以进行处理的线程)将被释放。但是,在此时请求仍处于挂起状态(因为await),因此IIS进程/应用程序域不会被回收。在内部,跟踪是通过AsyncManager完成的。但是,在ASP.NET中,像await Task.Run(DoWork)这样的事情是没有意义的:它只会释放一个线程并获取另一个线程。 - noseratio - open to work

1
如果任务是独立的(不依赖于其他任务的数据)并且可以独立执行,那么请务必异步执行它们。如果它们属于 Entity Framework,并且是版本 6,则提供了 Async 方法可供调用,您不必将它们包装在 Task.Run 中。
即使任务不是独立的,您仍然可以对它们进行排序以在执行时使它们更有效率。
但是使用 Task.Run 还是不使用是一个重要的区别。Task.Run 将使用 ThreadPool 线程,如果它正在同步执行某些操作,则会阻塞,因此会减少应用程序可用的 ThreadPool 线程数。如果您有很多用户,并且正在执行许多任务,则可能会出现问题。
请尽量找到异步 API 而不使用 Task.Run。

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