Java中并发管道的策略

时间:2010-01-15 01:31:37

标签: java performance multithreading concurrency

考虑以下shell脚本:

gzip -dc in.gz | sed -e 's/@/_at_/g' | gzip -c > out.gz 

这有三个进程并行工作,解压缩流,修改它并重新压缩它。运行time我可以看到我的用户时间大约是我实时的两倍,这表明程序有效地并行运行。

我试图通过将每个任务放在它自己的线程中来用Java创建相同的程序。不幸的是,对于上面的示例,多线程Java程序仅比30% faster版本大约single threaded。我尝试过同时使用ExchangerConcurrentLinkedQueue。 ConcurrentLinkedQueue链接队列会引起很多争用,尽管所有三个线程通常都处于忙碌状态。交换器的争用较少,但更复杂,并且似乎不会让最慢的工作者在100%的时间内运行。

我试图找出一个纯Java解决方案来解决这个问题,而不是考虑其中一个字节代码编织框架或基于JNI的MPI。

大多数并发性研究和API都关注divide-and-conquer算法,使每个节点的工作正交且不依赖于先前的计算。另一种并发方法是管道方法,其中每个工作人员完成一些工作并将数据传递给下一个工作人员。

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

10米行文件的当前时间如下:

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%的改进,这是通过实时测量具有10m行测试数据的四核系统来实现的。目前的资源来自Bitbucket

5 个答案:

答案 0 :(得分:14)

首先,这个过程只会和最慢的一样快。如果时间分解是:

  • gunzip:1秒
  • sed:5秒
  • gzip:1秒

通过多线程,您将在中完成 5秒而不是7秒。

其次,不是使用您正在使用的队列,而是尝试复制您正在复制的内容的功能,并使用PipedInputStreamPipedOutputStream将进程链接在一起。

编辑:使用Java并发工具处理相关任务有几种方法。将它划分为线程。首先创建一个公共基类:

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);
  }
}

所以,例如,Unzip PART:

public class Unzip implements Worker {
  protected void run(InputStream in, OutputStream out) {
    ...
  }
}

以及SedZip等等。然后将它绑定在一起的是:

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
  }
}

我不确定你能做得多好,并且为每项工作编写的代码最少。然后你的代码变成:

public static processFile(String inputName, String outputName) {
  pipe(new FileInputStream(inputFile), new FileOutputStream(outputFile),
    new Zip(), new Sed(), new Unzip());
}

答案 1 :(得分:6)

我个人验证了所花费的时间,看起来阅读时间不到10%,阅读加上处理时间不到30%。 所以我采用了ParallelExchangerTest(代码中性能最佳)并将其修改为 只有2个线程,第一个线程读取&amp;替换,第二个线程写作。

以下是要比较的数据(在我的机器上运行ubuntu和1gb ram的Intel双核(不是core2))

  

<强>&GT;通过shell进行测试

     

真实的0m41.601s

     

用户0m58.604s

     

sys 0m1.032s

     

<强>&GT;测试ParallelExchangerTest

     

真正的1m55.424s

     

用户2m14.160s

     

sys 0m4.768s

     

<强>&GT; ParallelExchangerTestMod(2个线程)

     

真实1m35.524s

     

用户1m55.319s

     

sys 0m3.580s

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

  

<强>&GT; ParallelExchangerTestMod_Regex(2个主题)

     

真实1m12.781s

     

用户1m33.382s

     

sys 0m2.916s

现在我先走了一步,而不是一次读一行,我读了 char []缓冲区的各种大小和时间,(与正则表达式搜索/替换,) 我得到了这些数字

  

<强>&GT;测试ParallelExchangerTestMod_Regex_Buff(一次处理100个字节)

     

真实的1m13.804s

     

用户1m32.494s

     

sys 0m2.676s

     

<强>&GT;测试ParallelExchangerTestMod_Regex_Buff(时间处理500字节)

     

真正的1m6.286s

     

用户1m29.334s

     

sys 0m2.324s

     

<强>&GT;测试ParallelExchangerTestMod_Regex_Buff(时间处理800字节)

     

真实1m12.309s

     

用户1m33.910s

     

sys 0m2.476s

看起来500字节对于数据大小是最佳的。

我在这里分叉并获得了我的更改副本

https://bitbucket.org/chinmaya/java-concurrent_response/

答案 2 :(得分:3)

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

为防止阻塞,我建议放一个管道尺寸。

答案 3 :(得分:3)

鉴于你没有说你如何衡量经过的时间,我假设你使用的是:

time java org.egge.concurrent.SerialTest < in.gz > out.gz
time java org.egge.concurrent.ConcurrentQueueTest < in.gz > out.gz

问题在于你在这里测量两件事:

  1. JVM启动需要多长时间,
  2. 程序运行多长时间。
  3. 您只能使用代码更改来更改第二个。使用您提供的数字:

    Testing SerialTest
    real    0m6.736s
    user    0m6.924s
    sys     0m0.245s
    
    Testing ParallelExchangerTest
    real    0m4.967s
    user    0m7.491s
    sys     0m0.850s
    

    如果我们假设JVM启动需要三秒钟,那么“程序运行时间”分别为3.7秒和1.9秒,这几乎是100%的加速。我强烈建议您使用更大的数据集进行测试,以便最大限度地减少JVM启动对时序结果的影响。

    编辑:根据您对此问题的回答,您可能会遇到锁定争用。在java中解决这个问题的最佳方法可能是使用管道读取器和编写器,从管道中读取,一次一个字节,并用输入流中的'@'替换输入流中的任何"_at_"个字符。输出流。您可能会遇到这样的事实:每个字符串都被扫描三次,并且任何替换都需要构建一个新对象,并且该字符串最终会再次被复制。希望这会有所帮助...

答案 4 :(得分:0)

减少读取和对象的数量可以使我的性能提高10%以上。

但是java.util.concurrent的性能仍然有点令人失望。

ConcurrentQueueTest:

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);
   }
  }