タイムアウトでInputStreamから読み取ることは可能ですか?


147

具体的には、問題は次のようなメソッドを記述することです。

int maybeRead(InputStream in, long timeout)

ここで、戻り値は、「タイムアウト」ミリ秒以内にデータが利用できる場合はin.read()と同じであり、そうでない場合は-2です。メソッドが戻る前に、生成されたスレッドは終了する必要があります。

引数を回避するために、Sun(すべてのJavaバージョン)によって文書化されているように、ここではjava.io.InputStreamの件名を使用しています。これは見た目ほど単純ではないことに注意してください。以下は、Sunのドキュメントで直接サポートされているいくつかの事実です。

  1. in.read()メソッドは、中断できない場合があります。

  2. InputStreamをReaderまたはInterruptibleChannelでラップしても効果はありません。これらのクラスで実行できるのは、InputStreamのメソッドを呼び出すことだけだからです。これらのクラスを使用できた場合、同じロジックをInputStreamで直接実行するだけのソリューションを作成することができます。

  3. in.available()が0を返すことは常に許容されます。

  4. in.close()メソッドは、ブロックするか、何もしません。

  5. 別のスレッドを強制終了する一般的な方法はありません。

回答:


83

inputStream.available()の使用

System.in.available()が0を返すことは常に許容されます。

私は反対を見つけました-それは常に利用可能なバイト数の最良の値を返します。のJavadoc 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.

タイミング/古さのため、見積もりは避けられません。新しいデータが常に到着しているため、この数値は1回限りの過小評価になる可能性があります。ただし、次の呼び出しでは常に「追いつきます」-新しい呼び出しの瞬間に到着したものではなく、到着したすべてのデータを考慮しなければなりません。データがあるときに永続的に0を返すと、上記の条件が満たされなくなります。

最初の注意:InputStreamの具象サブクラスはavailable()を担当します

InputStream抽象クラスです。データソースがありません。利用可能なデータがあることは意味がありません。したがって、のjavadocにavailable()も次のように記載されています。

The available method for class InputStream always returns 0.

This method should be overridden by subclasses.

そして実際、具象入力ストリームクラスはavailable()をオーバーライドし、定数0ではなく意味のある値を提供します。

2番目の警告:Windowsで入力を入力するときは、必ずキャリッジリターンを使用してください。

を使用しているSystem.in場合、コマンドシェルがプログラムを渡したときにのみ、プログラムは入力を受け取ります。ファイルリダイレクト/パイプを使用している場合(例:somefile> java myJavaAppまたはsomecommand | java myJavaApp)、入力データは通常すぐに渡されます。ただし、手動で入力を入力すると、データの受け渡しが遅れる可能性があります。たとえば、Windowsのcmd.exeシェルでは、データはcmd.exeシェル内でバッファリングされます。データは、キャリッジリターン(control-mまたは<enter>)に続いて実行中のJavaプログラムにのみ渡されます。これは実行環境の制限です。もちろん、InputStream.available()は、シェルがデータをバッファリングしている限り0を返します-これは正しい動作です。その時点で利用可能なデータはありません。シェルからデータが利用可能になるとすぐに、メソッドは> 0の値を返します。注意:Cygwinはcmdを使用します。

最も簡単なソリューション(ブロッキングがないため、タイムアウトは不要)

これを使うだけです:

    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.

または同等に、

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

よりリッチなソリューション(タイムアウト期間内にバッファを最大限に満たす)

これを宣言してください:

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

次にこれを使用します:

    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
場合はis.available() > 1024、この提案は失敗します。確かにゼロを返すストリームがあります。たとえば最近までSSLSockets。これに頼ることはできません。
ローン侯爵

「is.available()> 1024」のケースは、readLengthによって具体的に処理されます。
グレンベスト

SSLSocketsに関するコメントが正しくありません-バッファにデータがない場合、利用可能な場合は0を返します 私の答えに従って。Javadoc:「ソケットにバッファリングされたバイトがなく、ソケットがcloseを使用して閉じられていない場合、availableは0を返します。」
Glen Best

@GlenBest SSLSocketに関する私のコメントは正しくありません。 最近まで [私の強調]は、常にゼロを返していました。あなたは現在について話している。私はJSSEの歴史全体について話しているのですが、2002年Java 1.4に最初に組み込まれる前から一緒に取り組んできました。
ローンの侯爵

whileループ条件を「while(is.available()> 0 && System.currentTimeMillis()<maxTimeMillis && bufferOffset <b.length){」に変更することで、CPUオーバーヘッドを大幅に節約できました。
Logic1

65

ストリームがソケットでサポートされていない(Socket.setSoTimeout()つまり、を使用できない)と仮定すると、このタイプの問題を解決する標準的な方法は、Futureを使用することだと思います。

次のエグゼキューターとストリームがあるとします。

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

一部のデータを書き込み、最後のデータを書き込み、ストリームを閉じる前に5秒間待機するライターがあります。

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

通常の読み方は以下の通りです。読み取りはデータに対して無期限にブロックされるため、これは5秒で完了します。

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

出力:

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

ライターが応答しないなど、より根本的な問題があった場合、リーダーは永久にブロックします。読み取りを将来ラップする場合、次のようにタイムアウトを制御できます。

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

出力:

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)

TimeoutExceptionをキャッチして、必要なクリーンアップを実行できます。


14
しかし、ブロッキングスレッドはどうですか?アプリケーションが終了するまでメモリに残りますか?私が正しい場合、これにより、アプリケーションの負荷が高い無限のスレッドが生成される可能性があり、さらにスレッドが占有およびブロックされているプールを使用することからさらにスレッドをブロックする可能性があります。私が間違っていたら訂正してください。ありがとうございました。
Muhammad Gelbana 2012

4
Muhammad Gelbana、そうです。ブロックしているread()スレッドは実行中のままで、それは問題です。私はこれを防ぐ方法を見つけました:タイムアウトに達したら、呼び出しスレッドから入力ストリームを閉じます(私の場合、入力ストリームが送られてくるandroid bluetoothソケットを閉じます)。これを行うと、read()呼び出しはすぐに返されます。まあ、私の場合は、int read(byte [])オーバーロードを使用し、そのオーバーロードをすぐに返します。多分int read()オーバーロードは、何が返されるかわからないのでIOExceptionをスローします...私の考えではそれが適切な解決策です。
Emmanuel Touzery

5
-1は、アプリケーションが終了するまでスレッドの読み取りがブロックされたままであるため。
Ortwin Angermeier、2013

11
@ortangこれは、「TimeoutExceptionをキャッチし、クリーンアップを実行する」という意味です。たとえば、読み取りスレッドを強制終了したい場合があります。... catch(TimeoutException e){executor.shutdownNow(); }
イアンジョーンズ

12
executer.shutdownNowスレッドを強制終了しません。影響を与えずに中断しようとします。クリーンアップは不可能であり、これは深刻な問題です。
Marko Topolnik 2014

22

InputStreamがSocketによってサポートされている場合は、setSoTimeoutを使用してソケットタイムアウト(ミリ秒単位)を設定できます。指定されたタイムアウト時間内にread()呼び出しのブロックが解除されない場合、SocketTimeoutExceptionがスローされます。

read()を呼び出す前に、ソケットでsetSoTimeoutを呼び出していることを確認してください。


18

盲目的にそれを受け入れるのではなく、問題の記述に疑問を投げかけます。コンソールまたはネットワークを介したタイムアウトのみが必要です。あなたが持っている後者の場合Socket.setSoTimeout()およびHttpURLConnection.setReadTimeout()その両方あなたが構築する際に必要とされるまさに、限り、あなたはそれらを正しく設定して/それらを取得しません。アプリケーションの後半にあるInputStreamだけを任意のポイントに残しておくと、設計が不十分になり、実装が非常に厄介になります。


10
読み取りがかなりの時間ブロックする可能性がある他の状況があります。たとえば、テープドライブ、リモートにマウントされたネットワークドライブ、またはバックエンドにテープロボットを備えたHFSから読み取る場合。(しかし、あなたの答えの主な推力は正しいです。)
スティーブンC

1
@StephenC +1あなたのコメントと例。例を追加すると、単純なケースとして、ソケット接続は正しく行われたが、データがDBからフェッチされたために読み取りの試行がブロックされたが、どういうわけか発生しなかった場合があります(DBが応答せず、クエリが実行されたとしましょう)ロック状態)。このシナリオでは、ソケットでの読み取り操作を明示的にタイムアウトする方法が必要です。
sactiw 2013年

1
InputStream抽象化の要点は、基礎となる実装について考えないことです。投稿された回答の長所と短所について議論するのはフェアです。しかし、問題の発言に疑問を呈することは、分散を助けることにはならない
透明度

2
InputStreamはストリームで機能し、ブロックしますが、タイムアウトメカニズムを提供しません。したがって、InputStream抽象化は、適切に設計された抽象化ではありません。したがって、ストリームでタイムアウトする方法を要求しても、それほど要求されることはありません。したがって、問題は非常に実用的な問題の解決策を求めることです。基礎となる実装のほとんどはブロックされます。それがまさにストリームの本質です。ソケット、ファイル、パイプは、ストリームの反対側で新しいデータを準備できない場合にブロックされます。
pellucide

2
@EJP。どうやってそれを手に入れたのか分かりません。私はあなたに同意しませんでした。問題文「InputStreamでタイムアウトする方法」は有効です。フレームワークはタイムアウトする方法を提供しないので、そのような質問をするのが適切です。
pellucide 2016年

7

私はJava NIOパッケージのクラスを使用していませんが、ここで役立つと思われます。具体的には、java.nio.channels.Channelsおよびjava.nio.channels.InterruptibleChannelです。


2
+1:OPがInputStreamだけで要求することを確実に行う方法があるとは思いません。ただし、nioはこの目的のために作成されました。
エディ

2
OPはすでに基本的にこれを除外しています。InputStreamは本質的にブロックされており、中断できない場合があります。
ローンの侯爵2012年

5

これは、System.inからNIO FileChannelを取得し、タイムアウトを使用してデータの可用性を確認する方法です。これは、質問で説明されている問題の特殊なケースです。コンソールで実行し、何も入力せず、結果を待ちます。WindowsおよびLinux上のJava 6で正常にテストされました。

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

興味深いことに、コンソールではなくNetBeans 6.5内でプログラムを実行すると、タイムアウトはまったく機能せず、実際にはゾンビスレッドを強制終了するためにSystem.exit()を呼び出す必要があります。何が起こるかは、reader.interrupt()の呼び出しでインタラプタスレッドがブロックする(!)別のテストプログラム(ここには表示されていません)がさらにチャネルを閉じようとしますが、それも機能しません。


JDK 1.6でもJDK 1.7でも、mac OSでは機能しません。割り込みは、読み取り中にリターンを押した後にのみ認識されます。
モストウスキー崩壊

4

jtが言ったように、NIOは最良の(そして正しい)ソリューションです。しかし、もし本当にあなたがInputStreamに行き詰まっているなら、あなたはどちらかをすることができます

  1. 排他的なジョブであるスレッドを生成して、InputStreamから読み取り、ブロックせずに元のスレッドから読み取ることができるバッファーに結果を置きます。ストリームのインスタンスが1つしかない場合は、これでうまくいくはずです。それ以外の場合は、リソースリークが発生する可能性がありますが、Threadクラスの非推奨メソッドを使用してスレッドを強制終了できる場合があります。

  2. isAvailableを使用して、ブロックせずに読み取ることができるデータを示します。ただし、場合によっては(ソケットの場合など)、isAvailableが0以外のものを報告するために読み取りをブロックする可能性があります。


5
Socket.setSoTimeout()同様に正しい、はるかに単純なソリューションです。またはHttpURLConnection.setReadTimeout()
ローンの侯爵'19年

3
@EJP-これらは特定の状況でのみ「同等に正しい」; たとえば、入力ストリームがソケットストリーム/ HTTP接続ストリームの場合。
スティーブンC

1
@Stephen C NIOは非ブロッキングであり、同じ状況下でのみ選択可能です。たとえば、ノンブロッキングファイルI / Oはありません。
ローンの侯爵

2
@EJPですが、ノンブロッキングパイプIO(System.in)があります。(ローカルディスク上の)ファイルのノンブロッキングI / Oはナンセンスです
不安定な

1
@EJPほとんど(すべて?)のUnices System.inは実際にはパイプであり(シェルでファイルに置き換えるように指示しなかった場合)、パイプとしては非ブロッキングにすることができます。
12

0

この答えに触発されましたに私はもう少しオブジェクト指向のソリューションを思いつきました。

これは、文字を読むつもりの場合にのみ有効です

BufferedReaderをオーバーライドして、次のようなものを実装できます。

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

これがほぼ完全な例です。

一部のメソッドで0を返していますが、ニーズに合わせて-2に変更する必要がありますが、BufferedReaderコントラクトでは0の方が適していると思います。何も問題は発生せず、0文字が読み取られるだけです。readLineメソッドは恐ろしいパフォーマンスキラーです。実際にreadLinを使用する場合は、まったく新しいBufferedReaderを作成する必要があります。 e。現在、スレッドセーフではありません。readLinesがラインを待っている間に誰かが操作を呼び出すと、予期しない結果が生じます

私は-2を返すのが好きではありません。EOSを考慮するためにint <0かどうかをチェックしているだけの人もいるので、例外をスローします。とにかく、これらのメソッドは「ブロックできない」と主張しているので、そのステートメントが実際に真であるかどうかを確認し、オーバーライドしないでください。

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

}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.