对于大规模并行计算,我倾向于使用执行程序和可调用程序。当我有数千个要计算的对象时,我觉得为每个对象实例化数千个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。
问题:
答案 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?
有。首先,您的分区代码(从List
到List<List>
)可以在Guava等集合工具中找到,具有更通用和防错的实现。
但不仅如此,我们还会想到两种模式:
如果你的计算是&#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());
}
其他人已经证明了这可能是这样的。
您是否了解不同的方法?
对于并发编程的不同考虑(没有明确的任务/线程处理),请查看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池),但每个实现的概念通常是相同的。唯一的例外是如果您考虑到非标准运行行为的特定任务。