web-dev-qa-db-fra.com

Pourquoi plus Java utilise-t-il PipedInputStream / PipedOutputStream?

J'ai découvert cet idiome récemment, et je me demande s'il manque quelque chose. Je ne l'ai jamais vu utilisé. Presque tout Java avec lequel j'ai travaillé à l'état sauvage favorise le slurping des données dans une chaîne ou un tampon, plutôt que quelque chose comme cet exemple (en utilisant HttpClient et les API XML par exemple):

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

Ce code utilise une technique de style de tuyauterie Unix pour éviter que plusieurs copies des données XML ne soient conservées en mémoire. Il utilise le flux de sortie HTTP Post et l'API DOM Load/Save pour sérialiser un document XML en tant que contenu de la demande HTTP. Pour autant que je sache, cela minimise l'utilisation de la mémoire avec très peu de code supplémentaire (juste les quelques lignes pour Runnable, PipedInputStream et PipedOutputStream).

Alors, quel est le problème avec cet idiome? S'il n'y a rien de mal avec cet idiome, pourquoi ne l'ai-je pas vu?

EDIT: pour clarifier, PipedInputStream et PipedOutputStream remplacent la copie tampon par tampon passe-partout qui apparaît partout, et ils vous permettent également de traiter les données entrantes en même temps que l'écriture des données traitées. Ils n'utilisent pas de tuyaux OS.

48
Steven Huwig

Depuis Javadocs :

En règle générale, les données sont lues à partir d'un objet PipedInputStream par un thread et les données sont écrites dans le PipedOutputStream correspondant par un autre thread. Tenter d'utiliser les deux objets à partir d'un seul thread n'est pas recommandé, car cela peut bloquer le thread.

Cela peut expliquer en partie pourquoi il n'est pas plus couramment utilisé.

Je suppose qu'une autre raison est que de nombreux développeurs ne comprennent pas son objectif/avantage.

45
matt b

Dans votre exemple, vous créez deux threads pour effectuer le travail qui pourrait être effectué par un seul. Et introduire des retards d'E/S dans le mix.

Avez-vous un meilleur exemple? Ou ai-je juste répondu à votre question.


Pour tirer certains des commentaires (du moins ma vision d'eux) dans la réponse principale:

  • La concurrence introduit de la complexité dans une application. Au lieu de traiter avec un seul flux linéaire de données, vous devez maintenant vous préoccuper du séquencement des flux de données indépendants. Dans certains cas, la complexité supplémentaire peut être justifiée, en particulier si vous pouvez exploiter plusieurs cœurs/processeurs pour effectuer un travail gourmand en ressources processeur.
  • Si vous êtes dans une situation où vous pouvez bénéficier d'opérations simultanées, il existe généralement un meilleur moyen de coordonner le flux de données entre les threads. Par exemple, en passant des objets entre des threads à l'aide d'une file d'attente simultanée, plutôt que d'envelopper les flux canalisés dans des flux d'objets.
  • Lorsqu'un flux canalisé peut être une bonne solution, c'est lorsque vous avez plusieurs threads effectuant un traitement de texte, à la manière d'un pipeline Unix (par exemple: grep | sort).

Dans l'exemple spécifique, le flux canalisé permet d'utiliser une classe d'implémentation RequestEntity existante fournie par HttpClient. Je crois qu'une meilleure solution consiste à créer une nouvelle classe d'implémentation, comme ci-dessous, car l'exemple est finalement une opération séquentielle qui ne peut pas bénéficier de la complexité et des frais généraux d'une implémentation simultanée. Alors que je montre RequestEntity comme une classe anonyme, la réutilisabilité indiquerait que ce devrait être une classe de première classe.

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

Moi aussi, j'ai découvert récemment les classes PipedInputStream/PipedOutputStream.

Je développe un plug-in Eclipse qui doit exécuter des commandes sur un serveur distant via SSH. J'utilise JSch et l'API Channel lit à partir d'un flux d'entrée et écrit dans un flux de sortie. Mais je dois alimenter les commandes via le flux d'entrée et lire les réponses d'un flux de sortie. C'est là qu'intervient 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

De plus, revenons à l'exemple d'origine: non, cela ne minimise pas non plus exactement l'utilisation de la mémoire. Les arborescences DOM sont construites, la mise en mémoire tampon en mémoire est effectuée - bien que ce soit mieux que les répliques de tableaux d'octets complets, ce n'est pas beaucoup mieux. Mais la mise en mémoire tampon dans ce cas sera plus lente; et un thread supplémentaire est également créé - vous ne pouvez pas utiliser la paire PipedInput/OutputStream à partir d'un seul thread.

Parfois, les PipedXxxStreams sont utiles, mais la raison pour laquelle ils ne sont pas davantage utilisés est que, souvent, ils ne sont pas la bonne solution. Ils sont ok pour la communication inter-threads, et c'est là que je les ai utilisés pour ce que ça vaut. C'est juste qu'il n'y a pas beaucoup de cas d'utilisation pour cela, étant donné comment SOA repousse la plupart de ces limites entre les services, plutôt qu'entre les threads.

4
StaxMan

Voici un cas d'utilisation où les tuyaux ont du sens:

Supposons que vous ayez une bibliothèque tierce, comme un mappeur xslt ou une bibliothèque crypto qui possède une interface comme celle-ci: doSomething (inputStream, outputStream). Et vous ne voulez pas mettre le résultat en mémoire tampon avant de l'envoyer sur le fil. Apache et d'autres clients interdisent l'accès direct au flux de sortie du câble. Le plus proche que vous pouvez obtenir est d'obtenir le flux de sortie - à un décalage, après l'écriture des en-têtes - dans un objet d'entité de demande. Mais comme c'est sous le capot, il ne suffit toujours pas de passer un flux d'entrée et un flux de sortie à la bibliothèque tierce. Les tuyaux sont une bonne solution à ce problème.

Soit dit en passant, j'ai écrit une inversion de l'API client HTTP d'Apache [PipedApacheClientOutputStream] qui fournit une interface OutputStream pour HTTP POST utilisant Apache Commons HTTP Client 4.3.4. Il s'agit d'un exemple où les flux canalisés peuvent avoir un sens.

2
Robert Christian

J'ai essayé d'utiliser ces cours il y a quelque temps pour quelque chose, j'oublie les détails. Mais j'ai découvert que leur mise en œuvre est fatalement viciée. Je ne me souviens pas de quoi il s'agissait, mais je me souviens sournoisement que c'était peut-être une condition de course, ce qui signifiait qu'ils étaient parfois bloqués (Et oui, bien sûr, je les utilisais dans des threads séparés: ils ne sont tout simplement pas utilisables dans un fil unique et n'ont pas été conçus pour l'être).

Je pourrais jeter un oeil à leur code source et voir si je peux voir quel pourrait être le problème.

2
Adrian Pronk

Les canaux Java.io ont trop de changement de contexte (lecture/écriture par octet) et leur homologue Java.nio nécessite que vous ayez une certaine expérience NIO et une utilisation appropriée des canaux et d'autres choses, c'est ma propre implémentation de canaux utilisant une file d'attente de blocage qui, pour un seul producteur/consommateur fonctionnera rapidement et évoluera bien:

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

Alors, quel est le problème avec cet idiome? S'il n'y a rien de mal avec cet idiome, pourquoi ne l'ai-je pas vu?

EDIT: pour clarifier, PipedInputStream et PipedOutputStream remplacent la copie tampon par tampon passe-partout qui apparaît partout, et ils vous permettent également de traiter les données entrantes en même temps que l'écriture des données traitées. Ils n'utilisent pas de tuyaux OS.

Vous avez indiqué ce qu'il fait, mais vous n'avez pas expliqué pourquoi vous le faites.

Si vous pensez que cela réduira les ressources utilisées (CPU/mémoire) ou améliorera les performances, cela ne fonctionnera pas non plus. Cependant, cela rendra votre code plus complexe.

Fondamentalement, vous avez une solution sans problème pour laquelle elle résout.

0
Peter Lawrey