[英]How do I run something parallel in Java?
我正在尝试打印一个范围内的所有可能组合。 例如,如果我的lowerBound
是 3,而我的max
是 5,我想要以下组合:(5,4 - 5,3 - 4,3)。 我已经用下面的helper()
function 实现了这个。
当然,如果我的最大值非常大,这是很多组合,这将需要很长时间。 这就是我尝试实现ForkJoinPool
的原因,以便任务并行运行。 为此,我创建了一个新的ForkJoinPool
。 然后我遍历 r 的所有可能值(其中 r 是组合中的数字数量,在上面的示例中r=3
)。 对于 r 的每个值,我创建了一个新的HelperCalculator
,它扩展了RecursiveTask<Void>
。 在那里我递归地调用helper()
function。 每次我调用它时,我都会创建一个新的HelperCalculator
并在其上使用.fork()
。
问题如下。 它没有正确生成所有可能的组合。 它实际上根本不产生任何组合。 我试过在calculator.join()
calculator.fork()
,但这只会无限持续,直到出现OutOfMemory
错误。
显然,我对 ForkJoinPool 有一些误解,但是在尝试了几天之后,我再也看不到什么了。
我的主function:
ForkJoinPool pool = (ForkJoinPool) Executors.newWorkStealingPool();
for (int r = 1; r < 25; r++) {
int lowerBound = 7;
int[] data = new int[r];
int max = 25;
calculator = new HelperCalculator(data, 0, max, 0, s, n, lowerBound);
pool.execute(calculator);
calculator.join();
}
pool.shutdown();
HelperCalculator class:
protected Void compute() {
helper(data, end, start, index, s, lowerBound);
return null;
}
//Generate all possible combinations
public void helper(int[] data , int end, int start, int index,int s, int lowerBound) {
//If the array is filled, print it
if (index == data.length) {
System.out.println(Arrays.toString(data));
} else if (start >= end) {
data[index] = start;
if(data[0] >= lowerBound) {
HelperCalculator calculator = new HelperCalculator(data,end, start-1, index+1, s, n, lowerBound);
calculator.fork();
calculators.add(calculator);
HelperCalculator calculator2 = new HelperCalculator(data, end, start-1, index, s, n, lowerBound);
calculator2.fork();
calculators.add(calculator2);
}
}
如何使每个HelperCalculator
并行运行,以便使用 ForkJoinPool 同时运行 23 个? 或者我应该使用不同的解决方案吗?
我试过在calculators
列表上调用join()
和isDone()
,但它并没有等待它正确完成,程序就退出了。
因为有人不懂算法,这里是:
public static void main(String[] args) {
for(int r = 3; r > 0; r--) {
int[] data = new int[r];
helper(data, 0, 2, 0);
}
}
public static void helper(int[] data , int end, int start, int index) {
if (index == data.length) {
System.out.println(Arrays.toString(data));
} else if (start >= end) {
data[index] = start;
helper(data, end, start - 1, index + 1);
helper(data, end, start - 1, index);
}
}
}
output 是:
[2, 1, 0]
[2, 1]
[2, 0]
[1, 0]
[2]
[1]
[0]
您正在分叉的一些任务尝试使用相同的数组来评估不同的组合。 您可以通过为每个任务创建一个不同的数组或将并行性限制为那些已经拥有自己的数组的任务(即具有不同长度的任务)来解决该问题。
但还有另一种可能性; 根本不要使用 arrays。 您可以将组合存储到int
值中,因为每个int
值都是位的组合。 这不仅节省了大量的 memory,而且您还可以通过增加值轻松迭代所有可能的组合,因为迭代所有int
数字也迭代所有可能的位组合¹。 我们唯一需要实现的是通过根据 position 将位解释为数字来为特定的int
值生成正确的字符串。
对于第一次尝试,我们可以采取简单的方法并使用已经存在的类:
public static void main(String[] args) {
long t0 = System.nanoTime();
combinations(10, 25);
long t1 = System.nanoTime();
System.out.println((t1 - t0)/1_000_000+" ms");
System.out.flush();
}
static void combinations(int start, int end) {
for(int i = 1, stop = (1 << (end - start)) - 1; i <= stop; i++) {
System.out.println(
BitSet.valueOf(new long[]{i}).stream()
.mapToObj(b -> String.valueOf(b + start))
.collect(Collectors.joining(", ", "[", "]"))
);
}
}
该方法使用独占结束,因此对于您的示例,您必须将其称为combinations(0, 3)
并且它将打印
[0]
[1]
[0, 1]
[2]
[0, 2]
[1, 2]
[0, 1, 2]
3 ms
当然,时间可能会有所不同
对于上面的combinations(10, 25)
示例,它会打印所有组合,然后在我的机器上打印3477 ms
。 这听起来像是一个优化的机会,但我们应该首先考虑哪些操作会带来哪些成本。
在这里,对组合的迭代已简化为微不足道的操作。 创建字符串的成本要高一个数量级。 但这与实际打印相比仍然算不了什么,实际打印包括将数据传输到操作系统,并且根据系统的不同,实际渲染可能会增加我们的时间。 由于这是在PrintStream
中持有锁的情况下完成的,因此尝试同时打印的所有线程都将被阻止,使其成为不可并行化的操作。
让我们通过创建一个新的PrintStream
来确定成本的一部分,禁用换行符的自动刷新并使用一个非常大的缓冲区,能够容纳整个 output:
public static void main(String[] args) {
System.setOut(new PrintStream(
new BufferedOutputStream(new FileOutputStream(FileDescriptor.out),1<<20),false));
long t0 = System.nanoTime();
combinations(10, 25);
long t1 = System.nanoTime();
System.out.flush();
long t2 = System.nanoTime();
System.out.println((t1 - t0)/1_000_000+" ms");
System.out.println((t2 - t0)/1_000_000+" ms");
System.out.flush();
}
static void combinations(int start, int end) {
for(int i = 1, stop = (1 << (end - start)) - 1; i <= stop; i++) {
System.out.println(
BitSet.valueOf(new long[]{i}).stream()
.mapToObj(b -> String.valueOf(b + start))
.collect(Collectors.joining(", ", "[", "]"))
);
}
}
在我的机器上,它按以下顺序打印一些东西
93 ms
3340 ms
显示代码在不可并行打印上花费了超过 3 秒,而在计算上只花费了大约 100 毫秒。 为了完整起见,下面的代码为String
生成降低了一级:
static void combinations(int start, int end) {
for(int i = 1, stop = (1 << (end - start)) - 1; i <= stop; i++) {
System.out.println(bits(i, start));
}
}
static String bits(int bits, int offset) {
StringBuilder sb = new StringBuilder().append('[');
for(;;) {
int bit = Integer.lowestOneBit(bits), num = Integer.numberOfTrailingZeros(bit);
sb.append(num + offset);
bits -= bit;
if(bits == 0) break;
sb.append(", ");
}
return sb.append(']').toString();
}
这将我机器上的计算时间减半,而对总时间没有明显影响,现在这不足为奇。
但是出于教育目的,忽略潜在加速的缺乏,让我们讨论如何并行化此操作。
顺序代码确实已经将任务转化为一种形式,归结为从开始值到结束值的迭代。 现在,我们将这段代码重写为一个ForkJoinTask
(或合适的子类),它表示具有开始和结束值的迭代。 然后,我们添加了将这个操作分成两部分的能力,方法是在中间分割范围,所以我们得到两个任务在范围的每一半上迭代。 这可以重复,直到我们决定有足够的潜在并行作业并在本地执行当前迭代。 在本地处理之后,我们必须等待我们拆分的任何任务完成,以确保根任务的完成意味着所有子任务的完成。
public class Combinations extends RecursiveAction {
public static void main(String[] args) {
System.setOut(new PrintStream(new BufferedOutputStream(
new FileOutputStream(FileDescriptor.out),1<<20),false));
ForkJoinPool pool = (ForkJoinPool) Executors.newWorkStealingPool();
long t0 = System.nanoTime();
Combinations job = Combinations.get(10, 25);
pool.execute(job);
job.join();
long t1 = System.nanoTime();
System.out.flush();
long t2 = System.nanoTime();
System.out.println((t1 - t0)/1_000_000+" ms");
System.out.println((t2 - t0)/1_000_000+" ms");
System.out.flush();
}
public static Combinations get(int min, int max) {
return new Combinations(min, 1, (1 << (max - min)) - 1);
}
final int offset, from;
int to;
private Combinations(int offset, int from, int to) {
this.offset = offset;
this.from = from;
this.to = to;
}
@Override
protected void compute() {
ArrayDeque<Combinations> spawned = new ArrayDeque<>();
while(getSurplusQueuedTaskCount() < 2) {
int middle = (from + to) >>> 1;
if(middle == from) break;
Combinations forked = new Combinations(offset, middle, to);
forked.fork();
spawned.addLast(forked);
to = middle - 1;
}
performLocal();
for(;;) {
Combinations forked = spawned.pollLast();
if(forked == null) break;
if(forked.tryUnfork()) forked.performLocal(); else forked.join();
}
}
private void performLocal() {
for(int i = from, stop = to; i <= stop; i++) {
System.out.println(bits(i, offset));
}
}
static String bits(int bits, int offset) {
StringBuilder sb = new StringBuilder().append('[');
for(;;) {
int bit=Integer.lowestOneBit(bits), num=Integer.numberOfTrailingZeros(bit);
sb.append(num + offset);
bits -= bit;
if(bits == 0) break;
sb.append(", ");
}
return sb.append(']').toString();
}
}
getSurplusQueuedTaskCount()
为我们提供了有关工作线程饱和的提示,换句话说,分叉更多作业是否有益。 返回的数字与阈值进行比较,阈值通常是一个较小的数字,作业越异构,因此,预期的工作量,当作业比其他作业更早完成时允许更多工作窃取的阈值应该越高。 在我们的案例中,预计工作量将非常平衡。
有两种拆分方式。 示例通常会创建两个或多个分叉子任务,然后将它们连接起来。 这可能导致大量任务只是在等待其他任务。 另一种方法是分叉一个子任务并改变当前任务,以代表另一个。 在这里,分叉的任务表示[middle, to]
范围,而当前任务被修改为表示[from, middle]
范围。
在 fork 足够多的任务后,剩余的范围在当前线程中本地处理。 然后,该任务将等待所有分叉的子任务,并进行一项优化:如果没有其他工作线程窃取它们,它将尝试取消分叉子任务,在本地处理它们。
这工作顺利,但不幸的是,正如预期的那样,它不会加速操作,因为最昂贵的部分是打印。
¹ 使用int
表示所有组合将支持的范围长度减少到 31,但请记住,这样的范围长度意味着2³¹ - 1
组合,这需要大量迭代。 如果这仍然是一个限制,您可以将代码更改为使用long
。 当时支持的范围长度为 63, 2⁶³ - 1
组合,足以让计算机一直忙到宇宙的尽头。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.