【问题标题】:Which ExecutorService is best for blocking IO tasks哪个ExecutorService最适合阻塞IO任务
【发布时间】:2022-08-07 23:42:30
【问题描述】:

假设我们有 n 个独立的阻塞 IO 任务,例如休息呼叫另一台服务器的任务。然后我们需要结合所有答案。每个任务的处理时间可以超过 10 秒。

  1. 我们可以按顺序处理它,最后花费~n*10 秒:

    Task1Ans task1 = service1.doSomething();
    Task2Ans task2 = service2.doSomething()
    ...
    return result;
    
  2. 另一种策略是使用 CompletableFuture 以并行方式处理它,并在所有任务上花费约 10 秒:

    CompletableFuture<Task1Ans> task1Cs = CompletableFuture.supplyAsync(() -> service1.doSomething(), bestExecutor);
    CompletableFuture<Task2Ans> task2Cs = CompletableFuture.supplyAsync(() -> service2.doSomething(), bestExecutor);
    return CompletableFuture.allOf(task1Cs, task2Cs)
       .thenApply(nothing -> {
           ...
           // combine task1, task2 into result object
           return result;
       }).join();
    

    第二种方法有好处,但我不明白哪种类型的线程池最适合这种任务:

    ExecutorService bestExecutor = Executors.newFixedThreadPool(30)   /// or Executors.newCachedThreadPool() or Executors.newWorkStealingPool()
    

    我的问题是哪个 ExecutorService 最适合处理 n 并行阻塞 IO 任务。

    标签: java executorservice completable-future


    【解决方案1】:

    在完全受 CPU 限制的任务上,使用比 CPU 内核更多的线程不会获得额外的性能。所以在这种场景下,8核/8线程CPU只需要8线程就可以发挥最大性能,多用就会损失性能。 IO 任务通常通过使用比 CPU 内核更多的线程来获得性能,因为在等待 IO 时 CPU 时间可用于做其他事情。但是,即使每个线程的 CPU 开销很低,但随着每个线程占用内存并导致缓存/上下文切换,扩展也会受到限制。

    鉴于您的任务受 IO 限制,并且您没有提供任何其他约束,您可能应该为每个 IO 任务运行不同的线程。您可以通过使用固定或缓存的线程池来实现这一点。

    如果您的 IO 任务数量非常大(数千个以上),您应该限制线程池的最大大小,因为您可能拥有太多线程等情况。

    如果您的任务受 CPU 限制,您应该再次将线程池限制为更小的大小。可以使用以下方法动态获取内核数:

    int cores = Runtime.getRuntime().availableProcessors();
    

    此外,正如您的 CPU 具有缩放限制一样,您的 IO 设备通常也具有缩放限制。你不应该超过这个限制,但是如果不测量就很难说限制在哪里。

    【讨论】:

    • 或使用较少的比所有内核,因为 JVM、操作系统、实用程序、监控应用程序、文件系统等都需要内核来执行它们。
    • 好的,但是哪个是 IO 任务的最佳选择。固定线程池或缓存线程池。我的建议是:fixedThreadPool - 是大量 IO 任务的首选。我们需要限制我们的负载。 cachedThreadPool - 优先用于那些 IO 任务很少出现的应用程序(但仍然可以尽可能快地获得结果)。而且我们不需要保留许多未使用的线程。我是对的还是我在某个地方弄错了?
    • 只要您了解 IO 代码的行为,cachedThreadPool 就非常好。
    • 有时系统在 IO 中比在 CPU 中更并行。就像前面有一个大型数据库和小型 Java 服务器一样。在这些系统中,您可以在完全受限于 CPU 的情况下提高吞吐量。就像在服务器 CPU 为 100% 时再运行一次查询一样。您付出的代价是服务器的延迟时间延长。它不会响应,但整个系统将通过额外的查询继续增加吞吐量。所以“正确”的选择主要是关于你得到了什么以及你想要实现什么。代码的并行化通常受内存限制。更多的任务需要更多的内存。
    • 现在我注意到您的答案侧重于 CPU 密集型任务。但问题中并非如此。该问题明确指出,这些任务是不是受 CPU 限制,他们正在对 Web 服务进行 REST 调用。
    【解决方案2】:

    项目织机

    您的情况适合使用为 Java 的未来版本提议的新特性:virtual threadsstructured concurrency。这些是Project Loom 的一部分。

    今天的 Java 线程被一对一地映射到主机操作系统线程上。当 Java 代码阻塞时,宿主线程阻塞。主机操作系统线程处于空闲状态,等待恢复执行。主机操作系统线程是重量级的,在 CPU 和内存方面都很昂贵。所以这种空转不是最佳的。

    相反,Project Loom 中的虚拟线程被多对一映射到主机操作系统线程上。当虚拟线程中的代码阻塞时,该任务被“停放”,留出给另一个虚拟线程的任务一些执行时间。这种虚拟线程的停放是在 JVM 中管理的,因此它在 CPU 和内存中都经过高度优化、非常快速、非常高效。因此,在通用硬件上运行的 Java 应用程序一次可以支持数千甚至数百万个虚拟线程。

    ExecutorService 在 Loom 中是 AutoCloseable。因此,我们可以使用 try-with-resources 将您的整批任务包含在 try ( ExecutorService es = Executors.newVirtualThreadPerTaskExecutor() ) { … submit tasks … } 中。一旦完成,控制流将从 try-with-resources 块中退出,并且您知道您的任务已完成。访问为您提交的每个任务返回的 Future 对象。不需要CompletableFuture

    Loom 功能现在正在 Java 19 中进行预览和孵化。

    有关详细信息,请参阅 Project Loom 团队成员的几篇文章、演示文稿和采访。其中包括罗恩·普雷斯勒和艾伦·贝特曼。

    【讨论】:

      【解决方案3】:

      如果我正确理解您的问题,对于上述行为,无论选择executorService,更重要的是您如何称呼您的executorService

      例如。

      ExecutorService executorService=Executors.newCachedThreadPool();
      executorService.invokeAll(..);
      

      现在在这里,invokeAll(..) 将阻塞,直到内部提供的所有任务都完成。 所以我觉得选择任何 ExecutorService 并致电invokeAll(..) 将适合您的要求。

      还请查看此SE Question,其中讨论了ExecutorCompletionServiceinvokeAll 的新Java 8 介绍。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-09-03
        • 2012-01-16
        • 2010-11-17
        • 2015-08-28
        相关资源
        最近更新 更多