【问题标题】:Run a process asynchronously and read from stdout and stderr异步运行进程并从 stdout 和 stderr 读取
【发布时间】:2014-08-01 18:08:30
【问题描述】:

我有一些代码运行一个进程并异步读取标准输出和标准错误,然后在进程完成时处理。它看起来像这样:

Process process = builder.start();

    Thread outThread = new Thread(() -> {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            // Read stream here
        } catch (Exception e) {
        }
    });

    Thread errThread = new Thread(() -> {
      try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
        // Read stream here
      } catch (Exception e) {
      }
    });

    outThread.start();
    errThread.start();

    new Thread(() -> {
      int exitCode = -1;
      try {
        exitCode = process.waitFor();
        outThread.join();
        errThread.join();
      } catch (Exception e) {
      }

    // Process completed and read all stdout and stderr here
    }).start();

我的问题是我使用 3 个线程来实现这个异步“运行并获取输出”任务 - 我不知道为什么,但我觉得使用 3 个线程感觉不对。我可以从线程池中分配线程,但这仍然会阻塞这些线程。

有什么我可以做的,也许是使用 NIO 来减少这个(1?)线程?我能想到的任何事情都会不断地旋转线程(除非我添加了一些睡眠),我也不想这样做......

注意:我确实需要边读边读(而不是在进程停止时),并且我确实需要将 stdin 与 stderr 分开,因此无法进行重定向。

【问题讨论】:

  • 我认为您不需要最后一个线程。除非您需要循环运行其中的多个。
  • @ElliottFrisch - 您在一个名为 runAsync(String command) 的方法中看到的内容我需要立即返回(它是异步的)。我将如何处理进程的退出代码以及在没有额外线程的情况下完成流的读取?
  • 鉴于此,我认为您需要所有三个线程。

标签: java multithreading


【解决方案1】:

由于您已指定需要随时读取输出,因此没有非多线程解决方案。

您可以将线程数减少到主线程之外的一个:

Process process = builder.start();
Thread errThread = new Thread(() -> {
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
      // Read stream here
    } catch (Exception e) {
    }
});
errThread.start();

try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
        // Read stream here
} catch (Exception e) {
}
// we got an end of file, so there can't be any more input.  Now we need to wait for stderr/process exit.

int exitCode = -1;
try {
    exitCode = process.waitFor();
    errThread.join();
} catch (Exception e) {
}

// Process completed

如果你真的不需要在进程结束之前处理错误/输出,你可以稍微简化一下,只像这样使用你的主线程:

    File stderrFile = File.createTempFile("tmpErr", "out");
    File stdoutFile = File.createTempFile("tmpStd", "out");
    try {
        ProcessBuilder builder = new ProcessBuilder("ls /tmp");
        Process p = builder.start();
        int exitCode = -1;
        boolean done = false;
        while (!done) {
            try {
                exitCode = p.waitFor();
                done = true;
            } catch (InterruptedException ie) {
                System.out.println("Interrupted waiting for process to exit.");
            }
        }
        BufferedReader err = new BufferedReader(new FileReader(stderrFile));
        BufferedReader in = new BufferedReader(new FileReader(stdoutFile));
        ....
    } finally {
        stderrFile.delete();
        stdoutFile.delete();
    }

如果您从正在调用的进程中生成大量输出,这可能不是一个好主意,因为它可能会耗尽磁盘空间......但它可能会稍微快一些,因为它不必旋转启动另一个线程。

【讨论】:

  • 如果您实际上没有从 err 中读取,并且在您的过程中可能会很快卡住。
  • 它正在读取两者。基本上这里唯一的改进是您不需要单独的线程来等待进程退出。在标准输出关闭之前它不能退出。
  • 这不是要走的路,因为它只适用于非常小的输出。如果输出比较大(可能超过1kB?),你的主线程肯定会卡住。
【解决方案2】:

假设您不介意要合并的输入和错误流,您只能使用一个线程:

builder.redirectErrorStream(true); //merge input and error streams
Process process = builder.start();

Thread singleThread = new Thread(() -> {
  int exitCode = -1;
  //read from the merged stream
  try (BufferedReader reader = 
              new BufferedReader(new InputStreamReader(process.getInputStream()))) {
    String line;
    //read until the stream is exhausted, meaning the process has terminated
    while ((line = reader.readLine()) != null) {
      System.out.println(line); //use the output here
    }
    //get the exit code if required
    exitCode = process.waitFor();
  } catch (Exception e) { }
}).start();

【讨论】:

  • 阅读底部的注释:)
  • 我错过了 - 我不认为你可以做很多事情。为什么一开始就使用 3 个线程会有问题?
【解决方案3】:

查看来自 OstermillerUtils 的 ExecHelper

这个想法是等待进程完成的线程不只是等待,如果有可用输入,它会从 stdout 和 stderr 读取输入,并定期检查进程是否完成。

如果您不对来自 stdout 和 stderr 的输入进行任何繁重的处理,您可能不需要额外的线程来处理输入。只需复制 ExecHelper 并添加一些额外的函数/方法来处理任何新输入。我之前做过这个,是为了在进程运行的时候显示进程输出,做起来并不难(但是我丢失了源代码)。

如果您确实需要单独的线程来处理输入,请确保在更新或读取这些缓冲区时同步输出和错误 StringBuffers。

您可能要考虑的另一件事是添加中止超时。实现起来有点困难,但对我来说非常有价值:如果一个进程花费太多时间,该进程就会被破坏,从而确保没有任何东西挂起。您可以找到一个旧的(过时的?)示例this gist

【讨论】:

    【解决方案4】:

    你必须妥协。以下是您的选择:

    A.您可以使用 2 个线程(而不是 3 个)来完成:

    第一个线程:

    1. stdout 读取直到readline 返回null
    2. 致电Process.waitFor()
    3. join线程#2

    第二个线程:

    1. stderr 读取直到readline 返回null

    B.合并流并使用 Debian 的 annotate-output 区分 2 个流

    http://manpages.debian.org/cgi-bin/man.cgi?query=annotate-output&sektion=1

    C.如果这是一个短暂的过程,请等待它的结束

    D.如果这是一个长期存在的过程,那么您可以在读者之间转来转去,并在两者之间进行一些睡眠。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-06-19
      • 1970-01-01
      • 1970-01-01
      • 2023-04-06
      • 2016-12-21
      • 1970-01-01
      • 1970-01-01
      • 2019-11-13
      相关资源
      最近更新 更多