何时应将任务视为“长时间运行”?

8
在处理任务时,一个经验法则似乎是线程池 - 通常由例如调用Task.Run()Parallel.Invoke()使用 - 应该用于相对较短的操作。在处理长时间运行的操作时,我们应该使用TaskCreationOptions.LongRunning标志,以避免堵塞线程池队列,即将工作推送到新创建的线程中。
但是什么是长时间运行的操作?从时间上来说,长时间是多久?在决定是否使用LongRunning时,除了预期任务持续时间之外,是否还有其他因素需要考虑,例如预期的CPU架构(频率、核数等)或从程序员角度尝试同时运行的任务数量?
例如,假设我有500个任务要在专用应用程序中处理,每个任务需要10-20秒完成。我应该只使用Task.Run(例如在循环中)启动所有500个任务,然后等待它们全部完成,可能作为LongRunning,同时保留默认的最大并发级别吗?另一方面,如果在这种情况下设置LongRunning,那么这是否会创建500个新线程,实际上会导致更多开销和更高的内存使用(由于额外的线程被分配)与省略LongRunning相比?这是假设在等待这500个任务时不会安排执行任何新任务。
我猜设置LongRunning的决定取决于在给定时间间隔内向线程池发出的请求数量,并且LongRunning应仅用于预计要花费大量时间来完成大多数线程池放置任务的任务 - 按定义,最多只有一小部分所有任务。换句话说,这似乎是一个排队和线程池利用率优化问题,如果可能的话,应该通过测试逐案解决。我正确吗?
4个回答

12

有点并不重要。问题并不是时间,而是你的代码在做什么。如果你正在进行异步I/O,那么你只会在各个请求之间使用线程的短暂时间。如果你正在做CPU工作...好吧,你正在使用CPU。没有“线程池饥饿”的问题,因为CPU被充分利用。

真正的问题是当你在做使用CPU的阻塞工作时。在这种情况下,线程池饥饿会导致CPU利用率不足-你说“我需要CPU来完成我的工作”,但实际上你并没有使用它。

如果你没有使用阻塞API,则使用Task.RunLongRunning没有任何意义。如果你必须将一些传统阻塞代码异步运行,则使用LongRunning可能是一个好主意。总工作时间并不像“你执行此操作的频率”那样重要。如果你基于用户单击GUI启动了一个线程,那么成本与点击按钮时已经包含的所有延迟相比微不足道,你可以完全使用LongRunning来避免线程池。如果你正在运行一个产生大量阻塞任务的循环...停止这样做。这是个坏主意:D

例如,假设没有异步API替代File.Exists。所以如果你发现这会给你带来麻烦(例如在有故障的网络连接上),你可以使用Task.Run来启动它-并且由于你没有进行CPU工作,你将使用LongRunning

相比之下,如果你需要做一些基本上是100% CPU工作的图像处理,那么操作花费多长时间并不重要-这不是一个LongRunning的事情。

最常使用LongRunning的场景是当你的“工作”实际上是老式的“循环,并定期检查是否应该完成某些操作,然后再次循环”。长时间运行,但99%的时间只是在某些等待句柄上阻塞或类似的东西。请注意,这仅在处理不需要CPU绑定但没有适当的异步API的代码时有用。例如,如果您需要编写自己的SynchronizationContext,可能会找到这样的内容。
现在,我们如何将其应用于您的示例?嗯,我们不能,除非有更多信息。如果您的代码是CPU绑定的,则需要使用Parallel.For和相关组件-这些组件确保您仅使用足够的线程来饱和CPU,并且可以使用线程池进行此操作。如果它是CPU绑定的...您除了使用LongRunning以外别无选择,如果您想并行运行任务。理想情况下,这种工作将由您可以安全调用并从自己的线程await Task.WhenAll(...)的异步调用组成。

谢谢您提供详细的答案。是的,我假设了CPU密集型任务。然而,在IO绑定任务的情况下,LongRunning与并行有什么关系呢?如果没有足够的空间,使用LongRunning = false来生成500个IO绑定任务只会将它们放在线程池中,而LongRunning = true会创建500个新线程 - 用户感知的并发性和响应性几乎相同,甚至在后一种情况下由于额外的线程创建开销而更糟糕? - w128
1
@w128 这是关于线程池如何分配新线程的问题。默认情况下,它根据您的 CPU 核心数进行平衡(通常是物理核心数量的两倍),非常适合 CPU 工作。当您需要 更多 线程时,线程池会根据需要分配它们,但会有延迟 - 如果我没记错,需要大约 2 秒钟才能将新线程添加到池中。因此,如果您一次向线程池添加了 500 个阻塞任务,至少需要 1000 秒才能使池具有足够的线程来同时处理它们所有。LongRunning 只受自身启动限制,速度要快得多。 - Luaan

5
当涉及到任务时,一个经验法则似乎是,线程池(通常由例如调用Task.Run()或Parallel.Invoke())应该用于相对较短的操作。当涉及长时间运行的操作时,我们应该设置TaskCreationOptions.LongRunning为true,以避免堵塞线程池队列,即将工作推送到新创建的线程上,就我所理解的是如此。
绝大多数情况下,您根本不需要使用LongRunning,因为线程池会在2秒后调整以适应“丢失”线程到长时间运行的操作。
LongRunning的主要问题在于它强制您使用非常危险的StartNew API。
换句话说,这似乎是一个排队和线程池利用优化问题,如果有必要,应该通过测试逐个解决。我理解正确吗?

是的。你在编写代码时不应该一开始就设置LongRunning。如果由于线程池注入速率而出现延迟,那么你可以小心地添加LongRunning


2
你的情况不应该使用TaskCreationOptions.LongRunning。我会使用Parallel.For

如果你要创建很多任务,就像你的情况一样,不应该使用LongRunning选项。它应该用于创建运行很长时间的几个任务。
顺便说一下,我从未在任何类似的场景中使用过此选项。

2
正如您所指出的,TaskCreationOptions.LongRunning的目的是允许线程池在一个任务运行了很长时间后仍然能够继续处理其他工作项。
至于何时使用它:
实际上并没有具体的长度...通常只有在通过性能测试发现不使用它会导致其他工作的处理出现长时间延迟时才会使用LongRunning。
来源: 这里

+1 因为这是唯一一个包含源链接的答案,该链接本身包含了来自微软的 Stephen Toub 的回复(你的引用)。此外,当决定是否在需要异步运行许多任务且 Parallel.For 不适用时使用 LongRunning 标志,并且需要进行 CPU 密集型、长时间运行的后台工作以响应每个 Web 请求时,这似乎是唯一需要考虑的建议。 - Dave Sexton

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