【问题标题】:When should a task be considered "long running"?何时应将任务视为“长时间运行”?
【发布时间】:2016-08-06 04:55:13
【问题描述】:

在处理任务时,经验法则似乎是线程池 - 通常由例如调用 Task.Run()Parallel.Invoke() - 应该用于相对较短的操作。在处理长时间运行的操作时,我们应该使用TaskCreationOptions.LongRunning 标志,以便 - 据我了解 - 避免阻塞线程池队列,即将工作推送到新创建的线程。

但究竟什么是长时间运行操作? 在时间方面有多长? 在决定是否使用LongRunning 时,除了预期的任务持续时间之外,是否还有其他因素需要考虑,例如预期的 CPU 架构(频率、核心数量,...)或从程序员的角度来看将尝试一次运行的任务数量?

例如,假设我有 500 个任务要在一个专用应用程序中处理,每个任务需要 10-20 秒才能完成。我是否应该使用 Task.Run 启动所有 500 个任务(例如在循环中),然后等待所有任务,可能是 LongRunning,同时保留默认的最大并发级别?再说一次,如果我在这种情况下设置LongRunning,与省略LongRunning相比,这不会创建500个新线程并且实际上会导致大量开销和更高的内存使用(由于分配了额外的线程)吗?这是假设在等待这 500 个任务时不会安排新任务执行。

我猜想设置LongRunning 的决定取决于在给定时间间隔内向线程池发出的请求数,并且LongRunning 应该只用于预计需要更长的时间的任务大多数线程池放置的任务——根据定义,至多是所有任务的一小部分。换句话说,这似乎是一个排队和线程池利用率优化问题,如果有的话,应该可以通过测试逐个解决。我说的对吗?

【问题讨论】:

    标签: c# .net task-parallel-library task threadpool


    【解决方案1】:

    没关系。问题不在于时间,而在于您的代码在做什么。如果您正在执行异步 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 绑定任务的情况下,为什么LongRunningparallelism 有任何关系?不会使用LongRunning = false 生成 500 个 IO 绑定任务,只需将它们放在线程池中(假设有足够的空间),而 LongRunning = true 将创建 500 个新线程 - 感知到的并发性和响应性与用户几乎相同,或者由于额外的线程创建开销而在后一种情况下更糟?
    • @w128 这是关于线程池如何分配新线程的问题。默认情况下,它会根据您的 CPU 内核数量(通常约为物理内核数量的两倍)进行平衡,这对 CPU 工作非常有用。当您需要 更多 个线程时,线程池会根据需要分配它们,但会有延迟 - IIRC 大约需要 2 秒才能将新线程添加到池中。因此,如果您一次将 500 个阻塞任务添加到线程池,则池至少需要 1000 秒才能有足够的线程来同时处理它们。 LongRunning 仅受旋转本身的限制,速度要快得多。
    【解决方案2】:

    在处理任务时,经验法则似乎是线程池 - 通常由例如调用 Task.Run() 或 Parallel.Invoke() - 应该用于相对较短的操作。当处理长时间运行的操作时,我们应该将 TaskCreationOptions.LongRunning 设置为 true,以便 - 据我了解 - 避免阻塞线程池队列,即将工作推送到新创建的线程。

    绝大多数时候,你根本不需要使用LongRunning,因为线程池会在 2 秒后调整为“丢失”一个线程以进行长时间运行的操作。

    LongRunning 的主要问题是它强制你使用very dangerous StartNew API

    换句话说,这似乎是一个排队和线程池利用率优化问题,如果有的话,应该可以通过测试逐个解决。我说的对吗?

    是的。首次编写代码时,您永远不应该设置LongRunning。如果您看到由于线程池注入率而导致的延迟,那么您可以小心添加LongRunning

    【讨论】:

      【解决方案3】:

      正如您所指出的,TaskCreationOptions.LongRunning 的目的是

      允许线程池继续处理工作项,即使一个任务运行了很长时间

      至于什么时候使用:

      它本身并不是一个特定的长度...如果您通过性能测试发现不使用它会导致其他工作处理的长时间延迟,您通常只会使用 LongRunning。

      Source

      【讨论】:

      • +1 作为唯一包含源链接的答案,该链接本身包含来自 Microsoft 的 Stephen Toub 的回复(您的报价)。此外,在决定是否将LongRunning 标志用于需要异步运行的许多任务以及Parallel.For 不适用时,这似乎是真正需要考虑的唯一建议;例如,在每个 Web 请求期间,CPU 密集型、长时间运行的后台工作。
      【解决方案4】:

      在您的情况下,您不应使用 TaskCreationOptions.LongRunning。我会使用Parallel.For

      LongRunning 选项在您要创建大量任务时不可用,就像您的情况一样。它用于创建将长时间运行的几个任务。

      顺便说一句,我从来没有在任何类似的场景中使用过这个选项。

      【讨论】:

        猜你喜欢
        • 2012-08-13
        • 2022-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-07-08
        • 2014-09-10
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多