大规模任务Runnable或Callable的替代模式

时间:2015-11-20 13:31:32

标签: java multithreading

对于大规模并行计算,我倾向于使用执行程序和可调用程序。当我有数千个要计算的对象时,我觉得为每个对象实例化数千个Runnables感觉不太好。

所以我有两种方法来解决这个问题:

I 即可。将工作负载分成少量x工作者,每个工作者都给出y对象。 (将对象列表拆分为每个y / x-size的x分区)

public static <V> List<List<V>> partitions(List<V> list, int chunks) {
      final ArrayList<List<V>> lists = new ArrayList<List<V>>();
      final int size = Math.max(1, list.size() / chunks + 1);
      final int listSize = list.size();
      for (int i = 0; i <= chunks; i++) {
         final List<V> vs = list.subList(Math.min(listSize, i * size), Math.min(listSize, i * size + size));
         if(vs.size() == 0) break;
         lists.add(vs);
      }
      return lists;
   }

II 即可。创建从队列中获取对象的x-worker。

问题:

  • 创造出数千个Runnables真的很贵而且要避免吗?
  • 是否有通用模式/建议如何通过解决方案 II
  • 来实现
  • 您是否了解不同的方法?

5 个答案:

答案 0 :(得分:5)

创建数千个Runnable(实现Runnable的对象)并不比创建普通对象更昂贵。

创建和运行数千个线程可能非常繁重,但您可以将Executors与线程池一起使用来解决此问题。

答案 1 :(得分:2)

至于不同的方法,您可能对java 8的parallel streams感兴趣。

答案 2 :(得分:1)

在这里结合各种答案:

  

创造数千个Runnables真的很贵并且要避免吗?

不,它本身并不存在。它是如何使它们执行可能证明是昂贵的(产生几千个线程肯定有它的成本)。 所以你不想这样做:

List<Computation> computations = ...
List<Thread> threads = new ArrayList<>();
for (Computation computation : computations) {
    Thread thread = new Thread(new Computation(computation));
    threads.add(thread);
    thread.start();
}
// If you need to wait for completion:
for (Thread t : threads) {
    t.join();
}

因为它会在操作系统资源(本机线程,每个堆栈上都有堆栈)方面造成不必要的代价,2)垃圾邮件操作系统调度程序具有大量并发工作负载,大多数情况下导致大量上下文切换和CPU级别的相关缓存失效3)是捕捉和处理异常的噩梦(你的线程应该定义一个未捕获的异常处理程序,你必须手动处理它。)

您可能更喜欢这样一种方法:有限的线程池(少数线程,#34;少数&#34;与您的CPU内核数量密切相关)处理许多Callable

List<Computation> computations = ...
ExecutorService pool = Executors.newFixedSizeThreadPool(someNumber)
List<Future<Result>> results = new ArrayList<>();
for (Computation computation : computations) {
    results.add(pool.submit(new ComputationCallable(computation));
}
for (Future<Result> result : results {
    doSomething(result.get);
}

重复使用有限数量的线程这一事实应该会产生非常好的改进。

  

是否有一个通用模式/建议如何通过解决方案II?

有。首先,您的分区代码(从ListList<List>)可以在Guava等集合工具中找到,具有更通用和防错的实现。

但不仅如此,我们还会想到两种模式:

  1. 使用带有Fork / Join任务的Fork / Join Pool(即,使用您的整个项目列表生成任务,并且每个任务将使用该列表的一半来分叉子任务,直到每个任务管理一个足够小的物品清单)。它是分而治之的。请参阅:http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ForkJoinTask.html
  2. 如果你的计算是&#34;从列表中添加整数&#34;,它可能看起来像(那里可能存在边界错误,我没有真正检查过):

    public static class Adder extends RecursiveTask<Integer> {
    protected List<Integer> globalList;
    protected int start;
    protected int stop;
    
    public Adder(List<Integer> globalList, int start, int stop) {
      super();
      this.globalList = globalList;
      this.start = start;
      this.stop = stop;
      System.out.println("Creating for " + start + " => " + stop);
    }
    
    @Override
    protected Integer compute() {
      if (stop - start > 1000) {
        // Too many arguments, we split the list
        Adder subTask1 = new Adder(globalList, start, start + (stop-start)/2);
        Adder subTask2 = new Adder(globalList, start + (stop-start)/2, stop);
        subTask2.fork();
        return subTask1.compute() + subTask2.join();
      } else {
        // Manageable size of arguments, we deal in place
        int result = 0;
        for(int i = start; i < stop; i++) {
          result +=i;
        }
        return result;
      }
    }
    }
    
    public void doWork() throws Exception {
    List<Integer> computation = new ArrayList<>();
    for(int i = 0; i < 10000; i++) {
      computation.add(i);
    }
    ForkJoinPool pool = new ForkJoinPool();
    
    RecursiveTask<Integer> masterTask = new Adder(computation, 0, computation.size());
    Future<Integer> future = pool.submit(masterTask);
    System.out.println(future.get());
    
    }
    
    1. 使用Java 8并行流来轻松启动多个并行计算(实际上,Java并行流可以回退到Fork / Join池)。
    2. 其他人已经证明了这可能是这样的。

        

      您是否了解不同的方法?

      对于并发编程的不同考虑(没有明确的任务/线程处理),请查看actor模式。 https://en.wikipedia.org/wiki/Actor_model 想到Akka是这种模式的流行实现......

答案 3 :(得分:0)

@Aaron是对的,你应该看看Java 8's parallel streams

void processInParallel(List<V> list) {
    list.parallelStream().forEach(item -> {
        // do something
    });
}

如果您需要指定chunks,则可以使用ForkJoinPool所述的here

void processInParallel(List<V> list, int chunks) {
    ForkJoinPool forkJoinPool = new ForkJoinPool(chunks);
    forkJoinPool.submit(() -> {
        list.parallelStream().forEach(item -> {
            // do something with each item
        });
    });
}

您还可以将functional interface作为参数:

 void processInParallel(List<V> list, int chunks, Consumer<V> processor) {
    ForkJoinPool forkJoinPool = new ForkJoinPool(chunks);
    forkJoinPool.submit(() -> {
        list.parallelStream().forEach(item -> processor.accept(item));
    });
}

或者用简写符号表示:

void processInParallel(List<V> list, int chunks, Consumer<V> processor) {
    new ForkJoinPool(chunks).submit(() -> list.parallelStream().forEach(processor::accept));
}

然后你会像使用它一样:

processInParallel(myList, 2, item -> {
    // do something with each item
});

根据您的需要,ForkJoinPool#submit()会返回ForkJoinTask的实例,这是Future,您可以使用它来检查状态或等待任务结束

您最有可能只希望ForkJoinPool实例化一次(不在每次方法调用时实例化它),然后重复使用它以防止在多次调用该方法时CPU阻塞。

答案 4 :(得分:0)

  

创造数千个Runnables真的很贵并且要避免吗?

完全没有,runnable / callable接口只有一种方法可以实现,每个任务中“额外”代码的数量取决于您运行的代码。但当然没有Runnable / Callable接口的错误。

  

是否有一个通用模式/建议如何通过解决方案II?

模式2比模式1更有利。这是因为模式1假设每个工作人员将在完全相同的时间完成。如果某些工作人员在其他工作人员之前完成,他们可能只是闲置,因为他们只能处理分配给每个工作人员的y / x大小队列。但是,在模式2中,您将永远不会有空闲的工作线程(除非到达工作队列的末尾并且numWorkItems&lt; numWorkers)。

使用首选模式(模式2)的一种简单方法是使用ExecutorService invokeAll(Collection<? extends Callable<T>> list)方法。

以下是一个示例用法:

List<Callable<?>> workList = // a single list of all of your work
ExecutorService es = Executors.newCachedThreadPool();
es.invokeAll(workList);

使用方便,直观,ExecutorService实现会自动为您使用解决方案2,因此您知道每个工作线程的使用时间最大化。

  

您是否了解不同的方法?

解决方案1和2是通用工作的两种常用方法。现在,有许多不同的实现可供您选择(例如java.util.Concurrent,Java 8并行流或Fork / Join池),但每个实现的概念通常是相同的。唯一的例外是如果您考虑到非标准运行行为的特定任务。