web-dev-qa-db-ja.com

なぜJavaコードはPipedInputStream / PipedOutputStreamを使用しないのですか?

最近このイディオムを発見しましたが、何か足りないものがあるのではないかと思っています。私はそれが使用されるのを見たことがありません。ほとんどすべてのJava私が野生で作業したコードは、この例のようなものよりも、文字列またはバッファにデータを丸みすることを好みます(たとえば、HttpClientおよびXML APIを使用):

    final LSOutput output; // XML stuff initialized elsewhere
    final LSSerializer serializer;
    final Document doc;
    // ...
    PostMethod post; // HttpClient post request
    final PipedOutputStream source = new PipedOutputStream();
    PipedInputStream sink = new PipedInputStream(source);
    // ...
    executor.execute(new Runnable() {
            public void run() {
                output.setByteStream(source);
                serializer.write(doc, output);
                try {
                    source.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }});

    post.setRequestEntity(new InputStreamRequestEntity(sink));
    int status = httpClient.executeMethod(post);

このコードは、Unixパイピングスタイルの手法を使用して、XMLデータの複数のコピーがメモリに保持されるのを防ぎます。 HTTP Postの出力ストリームとDOM Load/Save APIを使用して、HTTP要求のコンテンツとしてXMLドキュメントをシリアル化します。私が知る限り、追加コードをほとんど使用せずにメモリの使用を最小限に抑えています(RunnablePipedInputStream、およびPipedOutputStreamの数行のみ)。

では、このイディオムの何が問題になっていますか?このイディオムに何も問題がない場合、なぜ私はそれを見ていないのですか?

編集:明確にするために、PipedInputStreamPipedOutputStreamはどこにでも現れるボイラープレートバッファごとのコピーを置き換えます。また、処理されたデータを書き出すと同時に受信データを処理できます。 OSパイプは使用しません。

48
Steven Huwig

Javadocs から:

通常、データは1つのスレッドによってPipedInputStreamオブジェクトから読み取られ、データは他のスレッドによって対応するPipedOutputStreamに書き込まれます。単一のスレッドから両方のオブジェクトを使用しようとすると、スレッドがデッドロックする可能性があるため、お勧めしません。

これにより、一般的に使用されない理由の一部が説明される場合があります。

もう一つの理由は、多くの開発者がその目的/利点を理解していないからだと思います。

45
matt b

この例では、2つのスレッドを作成して、1つのスレッドで実行できる作業を実行しています。そして、I/O遅延をミックスに導入します。

より良い例はありますか?または、私はあなたの質問に答えました。


一部のコメント(少なくとも私の意見)をメインの応答に取り込むには:

  • 並行性は、アプリケーションに複雑さをもたらします。データの単一の線形フローを処理する代わりに、独立したデータフローの順序付けを考慮する必要があります。場合によっては、特に複数のコア/ CPUを活用してCPU集中型の作業を行うことができる場合、追加された複雑さが正当化されることがあります。
  • 並行操作の恩恵を受けることができる状況にある場合は、通常、スレッド間のデータの流れを調整するより良い方法があります。たとえば、オブジェクトストリームでパイプストリームをラップするのではなく、並行キューを使用してスレッド間でオブジェクトを渡します。
  • パイプストリームが適切なソリューションになる可能性があるのは、テキスト処理を実行する複数のスレッドがある場合です(例:grep | sort)。

特定の例では、パイプストリームにより、HttpClientが提供する既存のRequestEntity実装クラスを使用できます。この例は、最終的には並行実装の複雑さとオーバーヘッドの恩恵を受けられないシーケンシャル操作であるため、以下のように新しい実装クラスを作成することがより良い解決策だと思います。 RequestEntityを匿名クラスとして示していますが、再利用可能性は、それがファーストクラスであるべきであることを示します。

post.setRequestEntity(new RequestEntity()
{
    public long getContentLength()
    {
        return 0-1;
    }

    public String getContentType()
    {
        return "text/xml";
    }

    public boolean isRepeatable()
    {
        return false;
    }

    public void writeRequest(OutputStream out) throws IOException
    {
        output.setByteStream(out);
        serializer.write(doc, output);
    }
});
7
kdgregory

私も最近PipedInputStream/PipedOutputStreamクラスを発見しました。

SSH経由でリモートサーバー上でコマンドを実行する必要があるEclipseプラグインを開発しています。 JSch を使用しており、Channel APIは入力ストリームから読み取り、出力ストリームに書き込みます。しかし、入力ストリームを介してコマンドをフィードし、出力ストリームから応答を読み取る必要があります。 PipedInput/OutputStreamの出番です。

import Java.io.PipedInputStream;
import Java.io.PipedOutputStream;

import com.jcraft.jsch.Channel;

Channel channel;
PipedInputStream channelInputStream = new PipedInputStream();
PipedOutputStream channelOutputStream = new PipedOutputStream();

channel.setInputStream(new PipedInputStream(this.channelOutputStream));
channel.setOutputStream(new PipedOutputStream(this.channelInputStream));
channel.connect();

// Write to channelInputStream
// Read from channelInputStream

channel.disconnect();
6
Brian Matthews

また、元の例に戻ります。いいえ、メモリ使用量を正確に最小化するわけでもありません。 DOMツリーが構築され、メモリ内のバッファリングが完了します。これは、フルバイト配列レプリカよりも優れていますが、それほど優れたものではありません。ただし、この場合のバッファリングは遅くなります。また、追加のスレッドも作成されます-単一のスレッド内からPipedInput/OutputStreamのペアを使用することはできません。

PipedXxxStreamsは便利な場合もありますが、あまり使用されない理由は、非常に頻繁に正しいソリューションではないためです。スレッド間通信には問題ありません。それが価値のあることのためにそれらを使用した場所です。 SOAがスレッド間ではなくサービス間でほとんどのそのような境界をプッシュする方法を考えると、この使用例がそれほど多くないというだけです。

4
StaxMan

パイプが意味をなすユースケースを次に示します。

Dosomething(inputStream、outputStream)のようなインターフェースを持つxsltマッパーや暗号ライブラリなどのサードパーティライブラリがあるとします。また、回線を介して送信する前に結果をバッファリングする必要はありません。 Apacheおよびその他のクライアントは、ワイヤー出力ストリームへの直接アクセスを許可しません。最も近いのは、ヘッダーが書き込まれた後のオフセットで、リクエストエンティティオブジェクトに出力ストリームを取得することです。しかし、これは内部にあるため、入力ストリームと出力ストリームをサードパーティのライブラリに渡すだけではまだ十分ではありません。パイプはこの問題の良い解決策です。

ちなみに、ApacheのHTTPクライアントAPIの反転を記述しました [PipedApacheClientOutputStream] HTTPのOutputStreamインターフェイスを提供しますPOST Apache Commons HTTP Client 4.3.4を使用しています。これはパイプストリームが意味をなす例。

2

しばらく前にこれらのクラスを使用してみましたが、詳細を忘れてしまいました。しかし、それらの実装には致命的な欠陥があることを発見しました。私はそれが何であったか思い出せませんが、それは時々デッドロックすることを意味する競合状態であったかもしれないという卑劣な記憶を持っています(そしてもちろん、私はそれらを別々のスレッドで使用していました:それらは単に単一のスレッドであるように設計されていませんでした)。

私は彼らのソースコードを見て、問題が何であったかを見ることができるかどうかを見るかもしれません。

2
Adrian Pronk

Java.ioパイプには、コンテキストの切り替えが多すぎる(バイト単位の読み取り/書き込み)ため、Java.nioの対応部分には、NIOのバックグラウンドとチャネルやものの適切な使用が必要です。これは、単一のプロデューサー/コンシューマーは高速でパフォーマンスが向上します。

import Java.io.IOException;
import Java.io.OutputStream;
import Java.util.concurrent.*;

public class QueueOutputStream extends OutputStream
{
  private static final int DEFAULT_BUFFER_SIZE=1024;
  private static final byte[] END_SIGNAL=new byte[]{};

  private final BlockingQueue<byte[]> queue=new LinkedBlockingDeque<>();
  private final byte[] buffer;

  private boolean closed=false;
  private int count=0;

  public QueueOutputStream()
  {
    this(DEFAULT_BUFFER_SIZE);
  }

  public QueueOutputStream(final int bufferSize)
  {
    if(bufferSize<=0){
      throw new IllegalArgumentException("Buffer size <= 0");
    }
    this.buffer=new byte[bufferSize];
  }

  private synchronized void flushBuffer()
  {
    if(count>0){
      final byte[] copy=new byte[count];
      System.arraycopy(buffer,0,copy,0,count);
      queue.offer(copy);
      count=0;
    }
  }

  @Override
  public synchronized void write(final int b) throws IOException
  {
    if(closed){
      throw new IllegalStateException("Stream is closed");
    }
    if(count>=buffer.length){
      flushBuffer();
    }
    buffer[count++]=(byte)b;
  }

  @Override
  public synchronized void write(final byte[] b, final int off, final int len) throws IOException
  {
    super.write(b,off,len);
  }

  @Override
  public synchronized void close() throws IOException
  {
    flushBuffer();
    queue.offer(END_SIGNAL);
    closed=true;
  }

  public Future<Void> asyncSendToOutputStream(final ExecutorService executor, final OutputStream outputStream)
  {
    return executor.submit(
            new Callable<Void>()
            {
              @Override
              public Void call() throws Exception
              {
                try{
                  byte[] buffer=queue.take();
                  while(buffer!=END_SIGNAL){
                    outputStream.write(buffer);
                    buffer=queue.take();
                  }
                  outputStream.flush();
                } catch(Exception e){
                  close();
                  throw e;
                } finally{
                  outputStream.close();
                }
                return null;
              }
            }
    );
  }
1
Guido Medina

では、このイディオムの何が問題になっていますか?このイディオムに何も問題がない場合、なぜ私はそれを見ていないのですか?

編集:明確にするために、PipedInputStreamとPipedOutputStreamは、どこにでも現れるボイラープレートバッファごとのコピーを置き換えます。また、処理されたデータの書き出しと同時に着信データを処理できます。 OSパイプは使用しません。

あなたはそれが何をするかを述べましたが、あなたがこれをしている理由を述べていません。

これにより、使用されるリソース(CPU /メモリ)が削減されるか、パフォーマンスが向上すると考えられる場合は、どちらも実行されません。ただし、コードがより複雑になります。

基本的に、問題のない解決策があります。

0
Peter Lawrey