【问题标题】:ThreadPoolExecutor#execute. How to reuse running threads?线程池执行器#execute。如何重用正在运行的线程?
【发布时间】:2018-04-02 17:45:01
【问题描述】:

多年来我一直使用 ThreadPoolExecutors,其中一个主要原因 - 由于并行性和“准备就绪”线程(虽然还有其他线程),它旨在“更快”处理许多请求。

现在我只关注以前众所周知的内部设计。
这是来自 java 8 ThreadPoolExecutor 的 sn-p:

public void execute(Runnable command) {
    ...
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     */
    ...
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
...

我对这第一步很感兴趣,因为在大多数情况下,您不希望线程轮询执行器将“未处理的请求”存储在内部队列中,最好将它们留在外部输入 Kafka 主题/JMS 队列等中。因此,我通常将面向性能/并行性的执行器设计为具有零内部容量和“调用者运行拒绝策略”。您选择了一些合理的大量并行线程和核心池超时,而不是吓到其他人并显示价值有多大;)。我不使用内部队列,我希望任务越早开始处理越好,因此它已成为“固定线程池执行器”。因此,在大多数情况下,我处于方法逻辑的“第一步”之下。

问题来了:它真的不会“重用”现有线程,而是每次“低于核心大小”时都会创建新线程(大多数情况下)吗? “只有在所有其他人都很忙时才添加新的核心线程”而不是“当我们有机会在另一个线程创建上吸一会时”不是更好吗?我错过了什么吗?

【问题讨论】:

  • 我不可能是唯一一个阅读 DeadPoolExecutor 的人
  • 只有当您的“corePoolSize”非常大(大于您曾经处理的请求数)时,您才在第一步。这是因为每个启动的工作人员的工作人员计数都会增加(并且只有在线程停止时才会减少)
  • 我处于第一步,因为负载一直在“波动”,并且由于“核心线程超时”而等待线程退出。此外,负载通常不会那么大,需要创建所有核心线程。所以我可以看到这张图片:在 20 个核心线程中,它使用了 10 个,并且对于每个新提交它都会创建新线程,旧线程会因为超时而退出。
  • 不太明白这个问题 - 当然 它会创建它们 - 如果核心线程不存在 someone 必须创建然后.当线程都忙,并且您低于最大计数时,它也会创建一个新线程。不同之处在于,核心线程一直存在,直到池关闭并且其他线程有空闲超时。
  • 在你明白了这一点之后,投票赞成/反对通常是明智的......

标签: java multithreading


【解决方案1】:

doc 描述了 corePoolSize、maxPoolSize 和任务队列之间的关系,以及提交任务时会发生什么。

...但每次“低于核心大小...”时都会创建一个新的 [线程]

是的。来自文档:

在方法execute(Runnable)中提交新任务时,更少 比 corePoolSize 线程正在运行,创建一个新线程 处理请求,即使其他工作线程处于空闲状态。

只有在其他人都忙的时候才添加新的核心线程不是更好吗……

由于您不想使用内部队列,这似乎是合理的。所以将 corePoolSize 和 maxPoolSize 设置为相同。一旦创建线程的加速完成,就不会再创建了。

但是,如果外部队列的增长速度快于处理速度,则使用 CallerRunsPolicy 似乎会损害性能。

【讨论】:

    【解决方案2】:

    问题是:真的是这样,它不会“重用”现有线程,而是每次“低于核心大小”时都会创建新线程(大多数情况)?

    是的,这就是记录和编写代码的方式。

    我错过了什么吗?

    是的,我认为您错过了“核心”线程的全部意义。在Executors docs 中定义的核心线程是:

    ... threads to keep in the pool, even if they are idle.
    

    这就是定义。线程启动是一个重要的过程,因此如果池中有 10 个核心线程,则对池的前 10 个请求每个都会启动一个线程,直到所有核心线程都处于活动状态。这会将启动负载分散到前 X 个请求中。这不是关于完成任务,而是关于初始化 TPE 和分散线程创建负载。如果您不希望出现这种情况,可以致电 prestartAllCoreThreads()

    核心线程的全部目的是让已经启动并运行的线程可以立即处理任务。如果我们必须在每次需要时启动一个线程,就会有不必要的资源分配时间和线程启动/停止开销,占用计算和操作系统资源。如果您不想要核心线程,那么您可以让它们超时并支付启动时间。

    多年来我一直使用 ThreadPoolExecutors,其中一个主要原因 - 它旨在“更快”处理许多请求,因为它具有并行性和“准备就绪”线程(不过还有其他线程)。

    TPE 不一定“更快”。我们使用它是因为手动管理多个线程并与之通信很难且容易出错。这就是 TPE 代码如此强大的原因。是操作系统线程为我们提供了并行性。

    我不使用内部队列,我希望任务越早开始处理越好,

    线程程序的重点是最大化吞吐量。如果您在 4 核系统上运行 100 个线程并且任务是 CPU 密集型的,那么您将为增加的上下文切换付出代价,并且处理大量请求的总时间将会减少。您的应用程序也很可能与其他程序竞争服务器上的资源,如果有 100 多个作业尝试同时在线程池中运行,您不希望它慢到爬行。

    限制您的核心线程(即使它们成为“合理的大数量”)的全部意义在于有一个最佳数量的并发线程,可以最大限度地提高应用程序的整体吞吐量。很难找到最佳的核心线程大小,但如果可能的话,实验会有所帮助。

    这在很大程度上取决于任务中 CPU 与 IO 的程度。如果任务对慢速服务进行远程 RPC 调用,那么在池中拥有大量核心线程可能是有意义的。但是,如果它们主要是 CPU 任务,您将希望更接近 CPU/内核的数量,然后将其余任务排队。同样,这一切都与整体吞吐量有关。

    【讨论】:

    • 这就是为什么它被称为 'sane big amount' 来暗示'限制计数和上下文切换',但我已经用 3 个词解释了这一点,而你在 3 个段落中做了同样的事情
    • 我没有错过“核心线程”这一点,我的问题是为什么没有优化 TPE,当您有“空闲”核心线程时,它会优先创建新线程来执行任务但不重用现有的。
    • 因为您不了解“核心线程”是什么以及它们是如何工作的。它为每个请求启动一个新的核心线程的唯一原因是像我所说的那样分散初始化 TPE 所需的 CPU @MykhayloAdamovych。这不是为了完成工作。这是关于初始化 TPE。我在回答中更清楚地说明了这一点。
    • 我的答案是什么问题,它需要 -1?
    • 另外,我的答案是为其他人写的。这就是 SO 的意义所在。这不仅仅是针对您的特定帖子。
    【解决方案3】:

    要重用线程,需要以某种方式将任务转移到现有线程。
    这将我推向了同步队列和零核心池大小。

    return new ThreadPoolExecutor(0, maxThreadsCount,
            10L, SECONDS,
            new SynchronousQueue<Runnable>(),
            new BasicThreadFactory.Builder().namingPattern("processor-%d").build());
    

    我的“主要流程”上的“峰值”数量确实减少了 500 - 1500 (ms)。
    但这仅适用于零大小的队列。对于非零大小的队列问题仍然是开放的。

    【讨论】:

    • 这既没有回答问题,也没有提供任何有用的信息。如果没有,如上所述,使用在当前线程上执行的拒绝执行策略,这几乎没有任何用处。相反,您要做的是在等待工作人员接任务时阻塞当前线程 - 所以它确实会施加背压(这很好),但它会浪费工作(这很糟糕)。
    • 我试图用'核心线程'来做它们不是为它们设计的。但所需的功能是通过“溢出线程”实现的。这就是为什么这部分回答了这个问题并且很有用。阻塞客户端线程对我来说没什么大不了的,因为“大量”TPE 线程数意味着数据不会被更快地处理拥有更多处理线程通常“调用者运行策略”不会产生“溢出错误”和松动任务。我主要关心的是“正常负载”下的“恒定线程创建”,我已经解决了这个问题。
    猜你喜欢
    • 2014-08-27
    • 2017-12-29
    • 1970-01-01
    • 2018-12-11
    • 2021-04-11
    • 1970-01-01
    • 2014-06-14
    • 1970-01-01
    • 2013-04-05
    相关资源
    最近更新 更多