Почему в 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-piping, чтобы предотвратить сохранение нескольких копий данных XML в памяти. Он использует поток вывода HTTP Post и API загрузки/сохранения DOM для сериализации XML-документа в качестве содержимого HTTP-запроса. Насколько я могу сказать, что это сводит к минимуму использование памяти с очень небольшим дополнительным кодом (всего в несколько строк для Runnable, PipedInputStream и PipedOutputStream).

Итак, что не так с этой идиомой? Если в этой идиоме нет ничего плохого, почему я ее не видел?

EDIT: уточнить, PipedInputStream и PipedOutputStream заменить шаблонный буфер на буфере копию, которая показывает повсюду, и они также позволяют обрабатывать входящие данные одновременно с записью из обработанных данных. Они не используют каналы ОС.

Ответ 1

Из Javadocs:

Как правило, данные считываются из объекта PipedInputStream одним потоком, и данные записываются в соответствующий PipedOutputStream другим потоком. Попытка использовать оба объекта из одного потока не рекомендуется, так как это может затормозить поток.

Это может частично объяснить, почему он не используется чаще.

Я бы предположил, что другая причина заключается в том, что многие разработчики не понимают его цели/выгоды.

Ответ 2

В вашем примере вы создаете два потока для выполнения работы, которая может быть выполнена одним. И вводя задержки ввода-вывода в микс.

У вас есть лучший пример? Или я просто ответил на ваш вопрос.


Чтобы вытащить некоторые комментарии (по крайней мере, мой взгляд на них) в основной ответ:

  • Concurrency вводит сложность в приложение. Вместо того, чтобы иметь дело с одним линейным потоком данных, теперь вам нужно беспокоиться о последовательности независимых потоков данных. В некоторых случаях добавленная сложность может быть оправдана, особенно если вы можете использовать несколько ядер/процессоров для работы с интенсивным процессором.
  • Если вы находитесь в ситуации, когда вы можете воспользоваться параллельными операциями, обычно существует лучший способ координировать поток данных между потоками. Например, передача объектов между потоками с использованием параллельной очереди, а не обтекание потоками в потоках объектов.
  • Если поток с каналами может быть хорошим решением, это когда у вас есть несколько потоков, выполняющих текстовую обработку, a la unix-конвейер (например: grep | sort).

В конкретном примере потоковый поток позволяет использовать существующий класс реализации RequestEntity, предоставляемый HttpClient. Я считаю, что лучшим решением является создание нового класса реализации, как показано ниже, потому что пример - это, в конечном счете, последовательная операция, которая не может выиграть от сложности и накладных расходов при одновременной реализации. Пока я показываю 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);
    }
});

Ответ 3

Я тоже недавно обнаружил классы PipedInputStream/PipedOutputStream.

Я разрабатываю подключаемый модуль Eclipse, который должен выполнять команды на удаленном сервере через SSH. Я использую JSch, и 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();

Ответ 4

Кроме того, вернемся к исходному примеру: нет, это не точно минимизирует использование памяти. Дерево DOM построено, буферизация в памяти выполнена - в то время как это лучше, чем полные байтовые массивные реплики, это не намного лучше. Но буферизация в этом случае будет медленнее; и создается дополнительный поток - вы не можете использовать пару PipedInput/OutputStream из одного потока.

Иногда PipedXxxStreams полезны, но причина, по которой они больше не используются, заключается в том, что довольно часто они не являются правильным решением. Они в порядке для межпоточной связи и того, где я использовал их для чего это стоит. Это просто, что для этого не так много вариантов использования, поскольку SOA толкает большинство таких границ между службами, а не между потоками.

Ответ 5

Я пытался использовать эти классы некоторое время назад для чего-то, я забыл детали. Но я обнаружил, что их реализация смертельно ошибочна. Я не помню, что это было, но у меня есть скрытая память о том, что это может быть состояние гонки, что означало, что они иногда зашли в тупик (И да, конечно, я использовал их в отдельном потоке: они просто не могут использоваться в одна нить и не предназначены для использования).

Я мог бы взглянуть на их исходный код и посмотреть, могу ли я увидеть, что проблема могла быть.

Ответ 6

Здесь используется случай, когда трубы имеют смысл:

Предположим, у вас есть сторонняя библиотека lib, такая как xslt mapper или crypto lib, которая имеет такой интерфейс: doSomething (inputStream, outputStream). И вы не хотите буферировать результат перед отправкой по проводу. Apache и другие клиенты запрещают прямой доступ к выходному потоку. Ближе всего вы можете получить выходной поток - со смещением после записи заголовков - в объект объекта запроса. Но поскольку это находится под капотом, все еще недостаточно, чтобы передать входной поток и выходной поток в стороннюю lib. Трубы - хорошее решение этой проблемы.

Кстати, я написал инверсию Apache HTTP Client API [PipedApacheClientOutputStream], которая предоставляет интерфейс OutputStream для HTTP POST с использованием Apache Commons HTTP Client 4.3 +0,4. Это пример, когда Piped Streams может иметь смысл.

Ответ 7

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

Ответ 8

Итак, что случилось с этой идиомой? Если нет ничего плохого в этой идиоме, почему я этого не видел?

EDIT: уточнить, PipedInputStream и PipedOutputStream заменит скопируйте буферную копию по буферу, которая появляются повсюду, и они также позволяют обрабатывать входящие данные одновременно с выписыванием обработанных данных. Они не используют ОС трубы.

Вы заявили, что он делает, но не указали, почему вы это делаете.

Если вы считаете, что это приведет либо к сокращению используемых ресурсов (cpu/memory), либо к повышению производительности, то и этого не произойдет. Однако это сделает ваш код более сложным.

В принципе у вас есть решение без проблем, для которого оно решается.