È possibile leggere da un InputStream con un timeout?


147

In particolare, il problema è scrivere un metodo come questo:

int maybeRead(InputStream in, long timeout)

dove il valore restituito è uguale a in.read () se i dati sono disponibili entro millisecondi di "timeout" e -2 altrimenti. Prima che il metodo ritorni, tutti i thread generati devono uscire.

Per evitare argomenti, l'argomento qui java.io.InputStream, come documentato da Sun (qualsiasi versione di Java). Si prega di notare che non è così semplice come sembra. Di seguito sono riportati alcuni fatti che sono supportati direttamente dalla documentazione di Sun.

  1. Il metodo in.read () potrebbe non essere interrompibile.

  2. Il wrapping di InputStream in un Reader o InterruptibleChannel non aiuta, poiché tutte quelle classi possono fare è chiamare i metodi di InputStream. Se fosse possibile utilizzare quelle classi, sarebbe possibile scrivere una soluzione che esegua la stessa logica direttamente su InputStream.

  3. È sempre accettabile che in.available () restituisca 0.

  4. Il metodo in.close () potrebbe bloccare o non fare nulla.

  5. Non esiste un modo generale per uccidere un altro thread.

Risposte:


83

Utilizzo di inputStream.available ()

È sempre accettabile che System.in.available () restituisca 0.

Ho trovato il contrario: restituisce sempre il valore migliore per il numero di byte disponibili. Javadoc per InputStream.available():

Returns an estimate of the number of bytes that can be read (or skipped over) 
from this input stream without blocking by the next invocation of a method for 
this input stream.

Una stima è inevitabile a causa di tempismo / staleness. La cifra può essere una sottovalutazione una tantum perché arrivano costantemente nuovi dati. Tuttavia, "raggiunge" sempre la chiamata successiva: dovrebbe tenere conto di tutti i dati arrivati, escluso quello in arrivo al momento della nuova chiamata. Restituendo permanentemente 0 quando ci sono dati non riesce la condizione sopra.

Primo avvertimento: sottoclassi concrete di InputStream sono responsabili per disponibili ()

InputStreamè una classe astratta. Non ha un'origine dati. Non ha senso avere dati disponibili. Quindi, Javadoc peravailable() afferma anche:

The available method for class InputStream always returns 0.

This method should be overridden by subclasses.

E in effetti, le classi concrete del flusso di input sovrascrivono available (), fornendo valori significativi, non 0 costanti.

Secondo avvertimento: assicurarsi di utilizzare il ritorno a capo quando si digita l'input in Windows.

Se si utilizza System.in, il programma riceve input solo quando la shell dei comandi lo consegna. Se stai utilizzando il reindirizzamento dei file / pipe (ad esempio somefile> java myJavaApp o somecommand | java myJavaApp), i dati di input vengono generalmente consegnati immediatamente. Tuttavia, se si digita manualmente l'input, la consegna dei dati può essere ritardata. Ad esempio con la shell di cmd.exe di Windows, i dati vengono bufferizzati nella shell di cmd.exe. I dati vengono passati al programma java in esecuzione solo dopo il ritorno a capo (control-m o<enter> ). Questa è una limitazione dell'ambiente di esecuzione. Ovviamente, InputStream.available () restituirà 0 fintanto che la shell esegue il buffering dei dati - questo è un comportamento corretto; non ci sono dati disponibili a quel punto. Non appena i dati sono disponibili dalla shell, il metodo restituisce un valore> 0. NB: Cygwin utilizza cmd.

Soluzione più semplice (nessun blocco, quindi nessun timeout richiesto)

Usa questo:

    byte[] inputData = new byte[1024];
    int result = is.read(inputData, 0, is.available());  
    // result will indicate number of bytes read; -1 for EOF with no data read.

O in modo equivalente,

    BufferedReader br = new BufferedReader(new InputStreamReader(System.in, Charset.forName("ISO-8859-1")),1024);
    // ...
         // inside some iteration / processing logic:
         if (br.ready()) {
             int readCount = br.read(inputData, bufferOffset, inputData.length-bufferOffset);
         }

Soluzione più ricca (riempie al massimo il buffer entro il periodo di timeout)

Dichiara questo:

public static int readInputStreamWithTimeout(InputStream is, byte[] b, int timeoutMillis)
     throws IOException  {
     int bufferOffset = 0;
     long maxTimeMillis = System.currentTimeMillis() + timeoutMillis;
     while (System.currentTimeMillis() < maxTimeMillis && bufferOffset < b.length) {
         int readLength = java.lang.Math.min(is.available(),b.length-bufferOffset);
         // can alternatively use bufferedReader, guarded by isReady():
         int readResult = is.read(b, bufferOffset, readLength);
         if (readResult == -1) break;
         bufferOffset += readResult;
     }
     return bufferOffset;
 }

Quindi usa questo:

    byte[] inputData = new byte[1024];
    int readCount = readInputStreamWithTimeout(System.in, inputData, 6000);  // 6 second timeout
    // readCount will indicate number of bytes read; -1 for EOF with no data read.

1
Se is.available() > 1024questo suggerimento fallirà. Ci sono certamente flussi che restituiscono zero. SSLSocket per esempio fino a poco tempo fa. Non puoi fare affidamento su questo.
Marchese di Lorne,

Il caso 'is.available ()> 1024' viene trattato in modo specifico tramite readLength.
Glen Best

Commento su SSLSocket errato: restituisce 0 per disponibile se non sono presenti dati nel buffer. Secondo la mia risposta. Javadoc: "Se sul socket non sono presenti buffer nel byte e il socket non è stato chiuso usando close, allora disponibile restituirà 0."
Glen Best

@GlenBest Il mio commento su SSLSocket non è errato. Fino a poco tempo fa [la mia enfasi] veniva sempre restituito zero. Stai parlando del presente. Sto parlando dell'intera storia di JSSE, con cui ho lavorato da prima che fosse incluso per la prima volta in Java 1.4 nel 2002 .
Marchese di Lorne,

Modificando le condizioni del ciclo while su "while (is.available ()> 0 && System.currentTimeMillis () <maxTimeMillis && bufferOffset <b.length) {" mi ha risparmiato un sacco di sovraccarico della CPU.
Logic1

65

Supponendo che il tuo stream non sia supportato da un socket (quindi non puoi usarlo Socket.setSoTimeout()), penso che il modo standard di risolvere questo tipo di problema sia usare un Future.

Supponiamo che io abbia il seguente esecutore e flussi:

    ExecutorService executor = Executors.newFixedThreadPool(2);
    final PipedOutputStream outputStream = new PipedOutputStream();
    final PipedInputStream inputStream = new PipedInputStream(outputStream);

Ho uno scrittore che scrive alcuni dati, quindi attende 5 secondi prima di scrivere l'ultimo pezzo di dati e chiudere il flusso:

    Runnable writeTask = new Runnable() {
        @Override
        public void run() {
            try {
                outputStream.write(1);
                outputStream.write(2);
                Thread.sleep(5000);
                outputStream.write(3);
                outputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    };
    executor.submit(writeTask);

Il modo normale di leggere questo è il seguente. La lettura si bloccherà indefinitamente per i dati e questo si completa in 5 secondi:

    long start = currentTimeMillis();
    int readByte = 1;
    // Read data without timeout
    while (readByte >= 0) {
        readByte = inputStream.read();
        if (readByte >= 0)
            System.out.println("Read: " + readByte);
    }
    System.out.println("Complete in " + (currentTimeMillis() - start) + "ms");

che produce:

Read: 1
Read: 2
Read: 3
Complete in 5001ms

Se ci fosse un problema più fondamentale, come lo scrittore che non risponde, il lettore si bloccherebbe per sempre. Se avvolgo la lettura in futuro, posso quindi controllare il timeout come segue:

    int readByte = 1;
    // Read data with timeout
    Callable<Integer> readTask = new Callable<Integer>() {
        @Override
        public Integer call() throws Exception {
            return inputStream.read();
        }
    };
    while (readByte >= 0) {
        Future<Integer> future = executor.submit(readTask);
        readByte = future.get(1000, TimeUnit.MILLISECONDS);
        if (readByte >= 0)
            System.out.println("Read: " + readByte);
    }

che produce:

Read: 1
Read: 2
Exception in thread "main" java.util.concurrent.TimeoutException
    at java.util.concurrent.FutureTask$Sync.innerGet(FutureTask.java:228)
    at java.util.concurrent.FutureTask.get(FutureTask.java:91)
    at test.InputStreamWithTimeoutTest.main(InputStreamWithTimeoutTest.java:74)

Posso catturare TimeoutException e fare tutto ciò che desidero.


14
Ma per quanto riguarda il thread di blocco ?! Rimarrà in memoria fino al termine dell'applicazione? Se ho ragione, questo potrebbe produrre thread infiniti l'applicazione è pesante e ancora di più, bloccare ulteriori thread dall'uso del pool che ha i thread occupati e bloccati. Per favore correggimi se sbaglio. Grazie.
Muhammad Gelbana,

4
Muhammad Gelbana, hai ragione: il thread read () bloccante rimane in esecuzione e non va bene. Ho trovato un modo per impedirlo però: quando il timeout scatta, chiudi il thread di input dal flusso di chiamata (nel mio caso chiudo la presa bluetooth android da cui proviene il flusso di input). Quando lo fai, la chiamata read () tornerà immediatamente .. Beh, nel mio caso uso il sovraccarico int read (byte []) e quello ritorna immediatamente. Forse il sovraccarico int read () genererebbe una IOException dal momento che non so cosa sarebbe tornato ... Secondo me questa è la soluzione corretta.
Emmanuel Touzery,

5
-1 poiché la lettura dei thread rimane bloccata fino al termine dell'applicazione.
Ortwin Angermeier,

11
@ortang Questo è un po 'quello che intendevo per "catturare TimeoutException e fare qualunque pulizia ..." Ad esempio, potrei voler uccidere il thread di lettura: ... catch (TimeoutException e) {ecutor.shutdownNow (); }
Ian Jones,

12
executer.shutdownNownon ucciderà il thread. Tenterà di interromperlo, senza alcun effetto. Non è possibile effettuare la pulizia e questo è un problema serio.
Marko Topolnik,

22

Se InputStream è supportato da un Socket, è possibile impostare un timeout Socket (in millisecondi) utilizzando setSoTimeout . Se la chiamata read () non si sblocca entro il timeout specificato, verrà generata una SocketTimeoutException.

Assicurati di chiamare setSoTimeout sul Socket prima di effettuare la chiamata read ().


18

Vorrei mettere in discussione l'affermazione del problema piuttosto che accettarla alla cieca. Hai solo bisogno di timeout dalla console o sulla rete. Se quest'ultimo hai Socket.setSoTimeout()e HttpURLConnection.setReadTimeout()che entrambi fanno esattamente ciò che è richiesto, a condizione che li configuri correttamente quando li costruisci / acquisisci. Lasciarlo ad un punto arbitrario più tardi nell'applicazione quando tutto ciò che hai è InputStream è un design scadente che porta a un'implementazione molto imbarazzante.


10
Ci sono altre situazioni in cui una lettura potrebbe potenzialmente bloccarsi per un tempo significativo; ad es. durante la lettura da un'unità nastro, da un'unità di rete montata in remoto o da un HFS con un robot nastro sul retro. (Ma la spinta principale della tua risposta è giusta.)
Stephen C

1
@StephenC +1 per il tuo commento ed esempi. Per aggiungere altro esempio, un semplice caso potrebbe essere quello in cui le connessioni socket sono state eseguite correttamente ma il tentativo di lettura è stato bloccato poiché i dati dovevano essere recuperati dal DB ma in qualche modo non è accaduto (diciamo che il DB non rispondeva e la query è andata nello stato bloccato). In questo scenario è necessario disporre di un modo per il timeout esplicito dell'operazione di lettura sul socket.
sacro

1
Il punto centrale dell'astrazione InputStream è di non pensare all'implementazione sottostante. È giusto discutere dei pro e dei contro delle risposte postate. Ma, mettere in discussione l'affermazione del problema, non aiuterà la disussione
pellucide,

2
InputStream funziona su uno stream e si blocca, ma non fornisce un meccanismo di timeout. Quindi l'astrazione InputStream non è un'astrazione progettata in modo appropriato. Quindi chiedere un modo per il timeout su uno stream non richiede molto. Quindi la domanda è chiedere una soluzione a un problema molto pratico. La maggior parte delle implementazioni sottostanti verrà bloccata. Questa è l'essenza stessa di un flusso. Socket, File, Pipes si bloccheranno se l'altro lato del flusso non è pronto con nuovi dati.
pellucide,

2
@EJP. Non so come l'hai ottenuto. Non sono d'accordo con te. L'istruzione del problema "come eseguire il timeout su un InputStream" è valida. Poiché il framework non fornisce un modo per il timeout, è opportuno porre tale domanda.
pellucide

7

Non ho usato le classi dal pacchetto Java NIO, ma sembra che potrebbero essere di qualche aiuto qui. In particolare, java.nio.channels.Channels e java.nio.channels.InterruptibleChannel .


2
+1: Non credo che ci sia un modo affidabile per fare ciò che l'OP richiede solo con InputStream. Tuttavia, nio è stato creato per questo scopo, tra gli altri.
Eddie,

2
L'OP lo ha già praticamente escluso. I flussi di input sono intrinsecamente bloccanti e potrebbero non essere interrompibili.
Marchese di Lorne,

5

Ecco un modo per ottenere un FileChannel NIO da System.in e verificare la disponibilità dei dati utilizzando un timeout, che è un caso speciale del problema descritto nella domanda. Eseguilo dalla console, non digitare alcun input e attendere i risultati. È stato testato correttamente con Java 6 su Windows e Linux.

import java.io.FileInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedByInterruptException;

public class Main {

    static final ByteBuffer buf = ByteBuffer.allocate(4096);

    public static void main(String[] args) {

        long timeout = 1000 * 5;

        try {
            InputStream in = extract(System.in);
            if (! (in instanceof FileInputStream))
                throw new RuntimeException(
                        "Could not extract a FileInputStream from STDIN.");

            try {
                int ret = maybeAvailable((FileInputStream)in, timeout);
                System.out.println(
                        Integer.toString(ret) + " bytes were read.");

            } finally {
                in.close();
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    /* unravels all layers of FilterInputStream wrappers to get to the
     * core InputStream
     */
    public static InputStream extract(InputStream in)
            throws NoSuchFieldException, IllegalAccessException {

        Field f = FilterInputStream.class.getDeclaredField("in");
        f.setAccessible(true);

        while( in instanceof FilterInputStream )
            in = (InputStream)f.get((FilterInputStream)in);

        return in;
    }

    /* Returns the number of bytes which could be read from the stream,
     * timing out after the specified number of milliseconds.
     * Returns 0 on timeout (because no bytes could be read)
     * and -1 for end of stream.
     */
    public static int maybeAvailable(final FileInputStream in, long timeout)
            throws IOException, InterruptedException {

        final int[] dataReady = {0};
        final IOException[] maybeException = {null};
        final Thread reader = new Thread() {
            public void run() {                
                try {
                    dataReady[0] = in.getChannel().read(buf);
                } catch (ClosedByInterruptException e) {
                    System.err.println("Reader interrupted.");
                } catch (IOException e) {
                    maybeException[0] = e;
                }
            }
        };

        Thread interruptor = new Thread() {
            public void run() {
                reader.interrupt();
            }
        };

        reader.start();
        for(;;) {

            reader.join(timeout);
            if (!reader.isAlive())
                break;

            interruptor.start();
            interruptor.join(1000);
            reader.join(1000);
            if (!reader.isAlive())
                break;

            System.err.println("We're hung");
            System.exit(1);
        }

        if ( maybeException[0] != null )
            throw maybeException[0];

        return dataReady[0];
    }
}

È interessante notare che quando si esegue il programma all'interno di NetBeans 6.5 piuttosto che sulla console, il timeout non funziona affatto e la chiamata a System.exit () è effettivamente necessaria per uccidere i thread di zombie. Quello che succede è che il thread di interruzione blocca (!) Sulla chiamata a reader.interrupt (). Un altro programma di test (non mostrato qui) tenta inoltre di chiudere il canale, ma non funziona neanche.


non funziona su mac os, né con JDK 1.6 né con JDK 1.7. L'interruzione viene riconosciuta solo dopo aver premuto Invio durante la lettura.
Mostowski Collapse

4

Come detto jt, NIO è la soluzione migliore (e corretta). Se sei davvero bloccato con un InputStream, puoi comunque

  1. Genera un thread il cui lavoro esclusivo è leggere da InputStream e inserire il risultato in un buffer che può essere letto dal thread originale senza bloccarlo. Questo dovrebbe funzionare bene se hai sempre e solo un'istanza del flusso. Altrimenti potresti essere in grado di uccidere il thread usando i metodi obsoleti nella classe Thread, sebbene ciò possa causare perdite di risorse.

  2. Affidati è disponibile per indicare i dati che possono essere letti senza bloccare. Tuttavia in alcuni casi (come con Socket) può essere necessaria una lettura potenzialmente bloccante per isAvailable per segnalare qualcosa di diverso da 0.


5
Socket.setSoTimeout()è una soluzione ugualmente corretta e molto più semplice. Or HttpURLConnection.setReadTimeout().
Marchese di Lorne,

3
@EJP - questi sono solo "ugualmente corretti" in determinate circostanze; ad es. se il flusso di input è un flusso socket / flusso di connessione HTTP.
Stephen C,

1
@Stephen C NIO è solo non bloccante e selezionabile nelle stesse circostanze. Ad esempio, non esiste alcun I / O di file non bloccante.
Marchese di Lorne,

2
@EJP ma c'è una pipe IO non bloccante (System.in), l'I / O non bloccante per i file (sul disco locale) non ha senso
woky

1
@EJP Nella maggior parte (tutti?) Unices System.in è in realtà una pipe (se non hai detto alla shell di sostituirlo con il file) e come pipe può essere non bloccante.
Woky

0

Ispirato a questa risposta, ho trovato una soluzione un po 'più orientata agli oggetti.

Questo è valido solo se hai intenzione di leggere caratteri

Puoi sovrascrivere BufferedReader e implementare qualcosa del genere:

public class SafeBufferedReader extends BufferedReader{

    private long millisTimeout;

    ( . . . )

    @Override
    public int read(char[] cbuf, int off, int len) throws IOException {
        try {
            waitReady();
        } catch(IllegalThreadStateException e) {
            return 0;
        }
        return super.read(cbuf, off, len);
    }

    protected void waitReady() throws IllegalThreadStateException, IOException {
        if(ready()) return;
        long timeout = System.currentTimeMillis() + millisTimeout;
        while(System.currentTimeMillis() < timeout) {
            if(ready()) return;
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                break; // Should restore flag
            }
        }
        if(ready()) return; // Just in case.
        throw new IllegalThreadStateException("Read timed out");
    }
}

Ecco un esempio quasi completo.

Sto restituendo 0 su alcuni metodi, dovresti cambiarlo in -2 per soddisfare le tue esigenze, ma penso che 0 sia più adatto con il contratto BufferedReader. Non è successo niente di sbagliato, ha appena letto 0 caratteri. Il metodo readLine è un orribile killer delle prestazioni. È necessario creare un BufferedReader completamente nuovo se si desidera effettivamente utilizzare readLin e. Al momento, non è thread-safe. Se qualcuno invoca un'operazione mentre readLines è in attesa di una riga, produrrà risultati imprevisti

Non mi piace tornare -2 dove sono. Darei un'eccezione perché alcune persone potrebbero semplicemente controllare se int <0 per considerare EOS. Ad ogni modo, quei metodi sostengono che "non può bloccare", dovresti controllare se quell'affermazione è effettivamente vera e semplicemente non sovrascriverla.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.nio.CharBuffer;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

/**
 * 
 * readLine
 * 
 * @author Dario
 *
 */
public class SafeBufferedReader extends BufferedReader{

    private long millisTimeout;

    private long millisInterval = 100;

    private int lookAheadLine;

    public SafeBufferedReader(Reader in, int sz, long millisTimeout) {
        super(in, sz);
        this.millisTimeout = millisTimeout;
    }

    public SafeBufferedReader(Reader in, long millisTimeout) {
        super(in);
        this.millisTimeout = millisTimeout;
    }



    /**
     * This is probably going to kill readLine performance. You should study BufferedReader and completly override the method.
     * 
     * It should mark the position, then perform its normal operation in a nonblocking way, and if it reaches the timeout then reset position and throw IllegalThreadStateException
     * 
     */
    @Override
    public String readLine() throws IOException {
        try {
            waitReadyLine();
        } catch(IllegalThreadStateException e) {
            //return null; //Null usually means EOS here, so we can't.
            throw e;
        }
        return super.readLine();
    }

    @Override
    public int read() throws IOException {
        try {
            waitReady();
        } catch(IllegalThreadStateException e) {
            return -2; // I'd throw a runtime here, as some people may just be checking if int < 0 to consider EOS
        }
        return super.read();
    }

    @Override
    public int read(char[] cbuf) throws IOException {
        try {
            waitReady();
        } catch(IllegalThreadStateException e) {
            return -2;  // I'd throw a runtime here, as some people may just be checking if int < 0 to consider EOS
        }
        return super.read(cbuf);
    }

    @Override
    public int read(char[] cbuf, int off, int len) throws IOException {
        try {
            waitReady();
        } catch(IllegalThreadStateException e) {
            return 0;
        }
        return super.read(cbuf, off, len);
    }

    @Override
    public int read(CharBuffer target) throws IOException {
        try {
            waitReady();
        } catch(IllegalThreadStateException e) {
            return 0;
        }
        return super.read(target);
    }

    @Override
    public void mark(int readAheadLimit) throws IOException {
        super.mark(readAheadLimit);
    }

    @Override
    public Stream<String> lines() {
        return super.lines();
    }

    @Override
    public void reset() throws IOException {
        super.reset();
    }

    @Override
    public long skip(long n) throws IOException {
        return super.skip(n);
    }

    public long getMillisTimeout() {
        return millisTimeout;
    }

    public void setMillisTimeout(long millisTimeout) {
        this.millisTimeout = millisTimeout;
    }

    public void setTimeout(long timeout, TimeUnit unit) {
        this.millisTimeout = TimeUnit.MILLISECONDS.convert(timeout, unit);
    }

    public long getMillisInterval() {
        return millisInterval;
    }

    public void setMillisInterval(long millisInterval) {
        this.millisInterval = millisInterval;
    }

    public void setInterval(long time, TimeUnit unit) {
        this.millisInterval = TimeUnit.MILLISECONDS.convert(time, unit);
    }

    /**
     * This is actually forcing us to read the buffer twice in order to determine a line is actually ready.
     * 
     * @throws IllegalThreadStateException
     * @throws IOException
     */
    protected void waitReadyLine() throws IllegalThreadStateException, IOException {
        long timeout = System.currentTimeMillis() + millisTimeout;
        waitReady();

        super.mark(lookAheadLine);
        try {
            while(System.currentTimeMillis() < timeout) {
                while(ready()) {
                    int charInt = super.read();
                    if(charInt==-1) return; // EOS reached
                    char character = (char) charInt;
                    if(character == '\n' || character == '\r' ) return;
                }
                try {
                    Thread.sleep(millisInterval);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt(); // Restore flag
                    break;
                }
            }
        } finally {
            super.reset();
        }
        throw new IllegalThreadStateException("readLine timed out");

    }

    protected void waitReady() throws IllegalThreadStateException, IOException {
        if(ready()) return;
        long timeout = System.currentTimeMillis() + millisTimeout;
        while(System.currentTimeMillis() < timeout) {
            if(ready()) return;
            try {
                Thread.sleep(millisInterval);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // Restore flag
                break;
            }
        }
        if(ready()) return; // Just in case.
        throw new IllegalThreadStateException("read timed out");
    }

}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.