【问题标题】:Execute millions of Runnable's with small memory footprint以较小的内存占用执行数百万个 Runnable
【发布时间】:2019-02-04 10:08:03
【问题描述】:

我有 N 个作为 ID 的 long。对于每个 ID,我需要执行一个 Runnable(即,我不关心返回值)并等待它们全部完成。每个 Runnable 可能需要几秒钟到几分钟的时间,并行运行大约 100 个线程是安全的。

在我们当前的解决方案中,我们使用 Executors.newFixedThreadPool(),为每个 ID 调用 submit(),然后在每个返回的 Future 上调用 get()。

代码运行良好,而且非常简单,我不必处理线程、复杂的等待逻辑等。它有一个缺点:内存占用。

所有仍在排队的 Runnable 消耗内存(比 long 所需的多 8 个字节:这些是我的 Java 类具有一些内部状态),所有 N 个 Future 实例也消耗内存(这些是 Java 类状态也是如此,我只用于等待,但我不需要实际结果)。我查看了一个堆转储,我估计 N=1000 万会占用超过 1 GiB 的内存。数组中的 1000 万个 long 只会消耗 76 MiB。

有没有办法解决这个问题,只将 ID 保存在内存中,最好不求助于低级并发编程?

【问题讨论】:

  • 您只需要生成例如10k Runnables 并将它们提交给 executor,一旦完成一半,再生成 5k,依此类推。
  • 有没有办法只提交一个 Runnable 包装你的 Long 参数,并让你的 Runnable 将那么长的时间转换为具有内部状态的自定义 java 类?这将降低可运行队列的占用空间,并降低Futures 的占用空间,因为它们中的任何一个都不会保留您的“内部状态”。一句话:将重量级状态的创建推迟到 runnable 内部。
  • @GPI 会有所帮助,但空对象的开销是 16 个字节,所以仍然很多。
  • “我有 N 个多头” - 以什么形式?在文件中,还是在服务器中?

标签: java multithreading


【解决方案1】:

这是我通常使用 Producer/Consummer 模式和协调两者的 BlockingQueue 所做的事情,或者,如果我手头有项目的话,使用 Akka actor。

但我想我会建议一些更安静的东西,依赖于 Java 的 Stream 行为。

直觉是,流的延迟执行将用于限制工作单元、期货及其结果的创建。

public static void main(String[] args) {
    // So we have a list of ids, I stream it
    // (note : if we have an iterator, you could group it by a batch of, say 100,
    // and then flat map each batch)
    LongStream ids = LongStream.range(0, 10_000_000L);
    // This is were the actual tasks will be dispatched
    ExecutorService executor = Executors.newFixedThreadPool(4);

    // For each id to compute, create a runnable, which I call "WorkUnit"
    Optional<Exception> error = ids.mapToObj(WorkUnit::new)
             // create a parralel stream
             // this allows the stream engine to launch the next instructions concurrently
            .parallel()
            // We dispatch ("parallely") the work units to a thread and have them execute
            .map(workUnit -> CompletableFuture.runAsync(workUnit, executor))
            // And then we wait for the unit of work to complete
            .map(future -> {
                try {
                    future.get();
                } catch (Exception e) {
                    // we do care about exceptions
                    return e;
                } finally {
                    System.out.println("Done with a work unit ");
                }
                // we do not care for the result
                return null;
            })
            // Keep exceptions on the stream
            .filter(Objects::nonNull)
            // Stop as soon as one is found
            .findFirst();


    executor.shutdown();
    System.out.println(error.isPresent());
}

说实话,我不确定规范是否保证了这种行为,但根据我的经验,它是有效的。每个并行的“chunck”获取几个 id,将其馈送到管道(映射到工作单元,分派到线程池,等待结果,过滤异常),这意味着很快就达到了平衡平衡活动工作单元的数量与executor 的数量。

如果要微调并行“块”的数量,应该在这里跟进:Custom thread pool in Java 8 parallel stream

【讨论】:

  • 流来救援!这似乎是最简单的解决方案。吞吐量只有一个问题。我创建了 100 个线程的执行程序,但我仍然受到流使用的常见 ForkJoinPool 的限制。对于 12 个 CPU,它只能达到 64 个线程的并行度。将默认池大小覆盖为 100 可解决此问题。见stackoverflow.com/a/29272776/552139
【解决方案2】:

是的:你可以有一个共享的 long 队列。你向执行器提交 n 个Runnables,其中 n 是执行器中的线程数,在 run 方法结束时,你从队列中获取下一个 long,并重新提交一个新的 Runnable

【讨论】:

  • 好的开始,但是我仍然需要手动实现错误处理,等待完成的代码仍然很棘手。
  • @fejesjoco 一点也不
【解决方案3】:

与其创建数百万个 Runnable,不如创建特定的线程池,将 long 作为任务。 不要使用 Future.get() 等待任务完成,而是使用 CountdownLatch。

那个线程池可以这样实现:

int N = 1000000;// number of tasks;
int T = 100; // number of threads;
CountdownLatch latch = new CountdownLatch(N);
ArrayBlockingQueue<Long> queue = new ArrayBlockingQueue<>();

for (int k=0; k<N; k++) {
   queue.put(createNumber(k));
}
for (int k=0; k<T; k++) {
  new WorkingThread().start();
}
CountdownLatch.await();

class WorkingThread extends Thread {
  public void run() {
      while (latch.getCount() != 0) {
           processNumber(queue.take());
           latch.countDown();
      }
  }
}

【讨论】:

  • 我认为您在获取工作单元时存在竞争条件。场景:N=1,T=1000,processNumber 需要 1 分钟。 1000 个线程将看到 latch.getCount() 为 1,但只有 1 个任务要获取,然后 999 个线程将在 queue.take() 中无限等待。虽然我确实认为消费者/生产者是解决这个问题的好方法。
  • 我们可以用检查队列是否为空然后退出来替换它。我想我会采用这个解决方案,但我仍然会使用这些线程的执行程序来为它们处理错误。
  • @fejesjoco 在问题的 cmets 中,您说在堆中“每个空对象 16 个字节”的 10M id 是“仍然很多”的开销。在这个解决方案中,你仍然有这 10M 对象在等待队列中......
  • @fejesjoco 你想要什么样的错误处理?结束第一个任务的整个流程,统计错误数,打印错误信息,还是重试失败的任务?
【解决方案4】:

使用ExecutorCompletionService 怎么样?类似于以下内容(可能包含错误,我没有测试过):

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorCompletionService;
import java.util.function.LongFunction;

public class Foo {

  private final ExecutorCompletionService<Void> completionService;
  private final LongFunction<Runnable> taskCreator;
  private final long maxRunning; // max tasks running or queued

  public Foo(Executor executor, LongFunction<Runnable> taskCreator, long maxRunning) {
    this.completionService = new ExecutorCompletionService<>(executor);
    this.taskCreator = taskCreator;
    this.maxRunning = maxRunning;
  }

  public synchronized void processIds(long[] ids) throws InterruptedException {
    int completed = 0;

    int running = 0;
    for (long id : ids) {
      if (running < maxRunning) {
        completionService.submit(taskCreator.apply(id), null);
        running++;
      } else {
        completionService.take();
        running--;
        completed++;
      }
    }

    while (completed < ids.length) {
      completionService.take();
      completed++;
    }

  }

}

上述的另一个版本可以使用SemaphoreCountDownLatch,而不是CompletionService

public static void processIds(long[] ids, Executor executor,
                              int max, LongFunction<Runnable> taskSup) throws InterruptedException {
  CountDownLatch latch = new CountDownLatch(ids.length);
  Semaphore semaphore = new Semaphore(max);

  for (long id : ids) {
    semaphore.acquire();

    Runnable task = taskSup.apply(id);
    executor.execute(() -> {
      try {
        task.run();
      } finally {
        semaphore.release();
        latch.countDown();
      }
    });

  }

  latch.await();
}

【讨论】:

  • 这与普通的 ExecutorService 解决方案非常相似,只是该类收集了 Future 的缓冲区而不是我们。所以可以不使用 ExecutorCompletionService 单独采用计数逻辑。但是这个解决方案的两个部分都很有趣。
  • 已编辑答案以添加稍微修改的选项。它基本上是一样的,但不需要阻塞队列(即ExecutorCompletionService)。不确定你是否认为SemaphoreCountDownLatch“低级”,但我不认为它过于复杂。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-18
  • 1970-01-01
  • 1970-01-01
  • 2014-12-12
  • 1970-01-01
  • 2019-05-09
相关资源
最近更新 更多