Java中并发管道的策略

Java中并发管道的策略,java,performance,multithreading,concurrency,Java,Performance,Multithreading,Concurrency,考虑以下shell脚本: gzip -dc in.gz | sed -e 's/@/_at_/g' | gzip -c > out.gz 这有三个并行工作的进程来解压缩、修改和重新压缩流。运行time我可以看到我的用户时间大约是我的实时时间的两倍,这表明程序有效地并行工作 我试图用Java创建相同的程序,方法是将每个任务放在它自己的线程中。不幸的是,多线程Java程序的版本仅比上述示例的版本高。我试过同时使用an和a。ConcurrentLinkedQueue链接队列会导致大量争用,尽

考虑以下shell脚本:

gzip -dc in.gz | sed -e 's/@/_at_/g' | gzip -c > out.gz 
这有三个并行工作的进程来解压缩、修改和重新压缩流。运行
time
我可以看到我的用户时间大约是我的实时时间的两倍,这表明程序有效地并行工作

我试图用Java创建相同的程序,方法是将每个任务放在它自己的线程中。不幸的是,多线程Java程序的版本仅比上述示例的版本高。我试过同时使用an和a。ConcurrentLinkedQueue链接队列会导致大量争用,尽管这三个线程通常都很忙。交换机具有较低的争用性,但更复杂,而且似乎无法让最慢的工作人员100%的时间运行

我试图找出一个解决这个问题的纯Java解决方案,而不看字节码编织框架或基于JNI的MPI

大多数并发研究和API都与算法有关,使每个节点的工作都是正交的,不依赖于先前的计算。另一种并发方法是管道方法,其中每个工作者都做一些工作,并将数据传递给下一个工作者

我并不是在试图找到最有效的方法来处理gzip文件,而是在研究如何有效地分解管道中的任务,以便将运行时间减少到最慢的任务的运行时间

10m线文件的当前计时如下所示:

Testing via shell

real    0m31.848s
user    0m58.946s
sys     0m1.694s

Testing SerialTest

real    0m59.997s
user    0m59.263s
sys     0m1.121s

Testing ParallelExchangerTest

real    0m41.573s
user    1m3.436s
sys     0m1.830s

Testing ConcurrentQueueTest

real    0m44.626s
user    1m24.231s
sys     0m10.856s

我为Java的10%改进提供了奖励,这是在一个具有1000万行测试数据的四核系统上实时测量的。电流源在上可用。

首先,该过程将只与最慢的工件一样快。如果时间细分为:

  • 爆炸时间:1秒
  • 赛德:5秒
  • gzip:1秒
通过使用多线程,最多只需5秒钟即可完成,而不是7秒钟

其次,与其使用您正在使用的队列,不如尝试复制您正在复制和使用的功能,并将流程链接在一起

编辑:使用Java并发UTIL处理相关任务有几种方法。把它分成线。首先创建一个公共基类:

public interface Worker {
  public run(InputStream in, OutputStream out);
}
这个接口的作用是表示处理输入并生成输出的任意作业。把这些链在一起,你就有了一条管道。你也可以抽象出样板文件。为此,我们需要一个类:

public class UnitOfWork implements Runnable {
  private final InputStream in;
  private final OutputStream out;
  private final Worker worker;

  public UnitOfWork(InputStream in, OutputStream out, Worker worker) {
    if (in == null) {
      throw new NullPointerException("in is null");
    }
    if (out == null) {
      throw new NullPointerException("out is null");
    }
    if (worker == null) {
      throw new NullPointerException("worker is null");
    }
    this.in = in;
    this.out = out;
    this.worker = worker;
  }

  public final void run() {
    worker.run(in, out);
  }
}
例如,
解压
部分:

public class Unzip implements Worker {
  protected void run(InputStream in, OutputStream out) {
    ...
  }
}
对于
Sed
Zip
,依此类推。然后将其结合在一起的是:

public static void pipe(InputStream in, OutputStream out, Worker... workers) {
  if (workers.length == 0) {
    throw new IllegalArgumentException("no workers");
  }
  OutputStream last = null;
  List<UnitOfWork> work = new ArrayList<UnitOfWork>(workers.length);
  PipedOutputStream last = null;
  for (int i=0; i<workers.length-2; i++) {
    PipedOutputStream out = new PipedOutputStream();
    work.add(new UnitOfWork(
      last == null ? in, new PipedInputStream(last), out, workers[i]);
    last = out;
  }
  work.add(new UnitOfWork(new PipedInputStream(last),
    out, workers[workers.length-1);
  ExecutorService exec = Executors.newFixedThreadPool(work.size());
  for (UnitOfWork w : work) {
    exec.submit(w);
  }
  exec.shutdown();
  try {
    exec.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
  } catch (InterruptedExxception e) {
    // do whatever
  }
}

您也可以在Java中使用管道。它们作为流实现,有关详细信息,请参阅和


为了防止堵塞,我建议使用支撑管尺寸。

鉴于您没有说明如何测量经过的时间,我假设您使用的是类似于:

time java org.egge.concurrent.SerialTest < in.gz > out.gz
time java org.egge.concurrent.ConcurrentQueueTest < in.gz > out.gz
如果我们假设JVM启动需要3秒,那么“程序运行时间”分别是3.7秒和1.9秒,这几乎是100%的加速。我强烈建议您使用更大的数据集进行测试,这样您就可以最大限度地减少JVM启动对计时结果的影响


编辑:根据您对这个问题的回答,您可能正遭受锁争用的痛苦。在java中解决这个问题的最佳方法可能是使用管道读取器和写入器,从管道中一次读取字节,并将输入流中的任何
@
字符替换为输出流中的
“\u at”
。您可能会遇到这样的情况:每个字符串被扫描三次,任何替换都需要构建一个新对象,并且字符串最终会再次被复制。希望这有助于

我单独验证了所花费的时间,似乎阅读所花费的时间不到10%,阅读加处理所花费的时间不到30%。 所以我采用了ParallelExchangerTest(代码中性能最好的)并将其修改为 只有两个线程,第一个线程执行读取和替换,第二个线程执行写入

下面是要比较的数字(在我的机器上,运行带有1gb内存的ubuntu的Intel双核(不是core2)

>通过shell进行测试

实数0m41.601s

用户0m58.604s

系统0m1.032s

>测试并行交换测试

real 1m55.424s

用户2m14.160s

系统0m4.768s

>ParallelExchangerTestMod(2线程)

real 1m35.524s

用户1m55.319s

系统0m3.580s

我知道字符串处理需要更长的时间,所以我将替换line.repalce 用matcher.replaceAll我得到了这个数字

>ParallelExchangerTestMod\u正则表达式(2线程)

实1m12.781s

用户1m33.382s

系统0m2.916s

现在我领先了一步,不是一次只读一行,而是阅读 各种大小的char[]缓冲区,并对其进行计时(使用regexp搜索/替换) 我有这些数字

>测试ParallelExchangerTestMod\u Regex\u Buff(一次处理100字节)

real 1m13.804s

用户1m32.494s

系统0m2.676s

>测试ParallelExchangerTestMod\u Regex\u Buff(每次处理500字节)

实1m6.286s

用户1m29.334s

sys 0m2.324s

>测试ParallelExchangerTestMod\u Regex\u Buff(一次处理800字节)

真正的1m12.309s

用户1m33.910s

系统0m2.476s

看起来500字节是数据大小的最佳选择

我用叉子叉了叉,这里有一份更改的副本


减少读取和对象的数量可以使性能提高10%以上
time java org.egge.concurrent.SerialTest < in.gz > out.gz
time java org.egge.concurrent.ConcurrentQueueTest < in.gz > out.gz
Testing SerialTest
real    0m6.736s
user    0m6.924s
sys     0m0.245s

Testing ParallelExchangerTest
real    0m4.967s
user    0m7.491s
sys     0m0.850s
private static class Reader implements Runnable {

@Override
  public void run() {
   final char buf[] = new char[8192];
   try {

    int len;
    while ((len = reader.read(buf)) != -1) {
     pipe.put(new String(buf,0,len));
    }
    pipe.put(POISON);

   } catch (IOException e) {
    throw new RuntimeException(e);
   } catch (InterruptedException e) {
    throw new RuntimeException(e);
   }
  }