我有一个Java程序需要处理一长串输入字符串。为此,它会遍历每个字符串,将其传递给
Process
(Python 脚本),从 Process
的 OutputStream
获取结果,然后移至下一个字符串。然而,我发现运行几个小时后,程序会冻结,Java 程序正在等待 Python 的输出。
为了调试,我制作了一个更简单的程序版本,它使用小字符串,不在 Java 端进行任何缓冲,也不修改 Python 脚本中的数据。但现在我发现它在不同的地方冻结了,Java 尝试将数据刷新到 Python 脚本,而 Python 脚本尝试将结果刷新到 Java。我发现在冻结之前能够处理的项目数量在程序运行之间略有不同,并且增加字符串的长度会大大减少可以处理的项目数量。
Java程序:
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
public class Main {
public static void main(String[] args) throws IOException, InterruptedException {
Process process = start("python", "test.py");
for (int i = 0; i < 1000; i++) {
System.out.println(i);
processText("test string test string test string test string ", process);
}
process.getOutputStream().close();
boolean finished = process.waitFor(10, SECONDS);
if (!finished) {
process.destroyForcibly();
}
}
public static Process start(String... command) throws IOException {
ProcessBuilder processBuilder = new ProcessBuilder(command);
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
return processBuilder.start();
}
public static String processText(String text, Process process) throws IOException {
byte[] bytes = (text + "\n").getBytes(UTF_8);
OutputStream outputStream = process.getOutputStream();
System.out.println("Writing...");
outputStream.write(bytes);
System.out.println("Done!");
outputStream.flush();
System.out.println("Reading...");
String result = readLn(process);
System.out.println("Got it!");
return result;
}
public static String readLn(Process process) throws IOException {
InputStream inputStream = process.getInputStream();
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte newlineByte = "\n".getBytes(UTF_8)[0];
byte lastByte = -1;
while (lastByte != newlineByte) {
lastByte = (byte) inputStream.read();
byteArrayOutputStream.write(lastByte);
}
return byteArrayOutputStream.toString(UTF_8);
}
}
Python脚本:
import sys
import io
in_stream = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
out_stream = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
def output(s):
out_stream.write(s)
text = in_stream.readline()
while text != "":
print("Outputting result", file=sys.stderr)
output(text)
print("Output done!", file=sys.stderr)
output("\n")
print("Flushing", file=sys.stderr)
out_stream.flush()
print("Flushed", file=sys.stderr)
text = in_stream.readline()
Java 的输出:
0
Writing...
Done!
Reading...
Got it!
1
Writing...
Done!
Reading...
Got it!
.
.
.
379
Writing...
Done!
Reading...
Got it!
380
Writing...
Done! [Freezes here]
Python 的输出(通过 stderr):
Outputting result
Output done!
Flushing
Flushed
.
.
.
Outputting result
Output done!
Flushing
Flushed
Outputting result
Output done!
Flushing [Freezes here]
当我强制 Java 程序停止时,我从 Python 的 stderr 获得额外的输出:
Flushed
Outputting result
Output done!
Flushing
Traceback (most recent call last):
File "...\test.py", line 17, in <module>
out_stream.flush()
OSError: [Errno 22] Invalid argument
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='cp1252'>
OSError: [Errno 22] Invalid argument
如果我使用
print()
和 input()
而不是 in_stream 和 out_stream 我可以设法完成所有 1000 个项目。不过,我想确保在 Java 和 Python 之间传递数据时使用 UTF-8 编码,这样我就可以拥有所有 Unicode 字符并且不会丢失任何数据。这就是为什么我根据我在网上阅读的内容使用TextIOWrapper
(我认为这将是处理大量数据的最有效方法)。虽然最后一个错误输出似乎是说它使用的是 cp1252 而不是 UTF-8?我该如何解决这个问题?
我使用的是 Windows 10、Java 17 和 Python 3.10
编辑:我认为错误是说 sys.stdout 有 cp1252 编码; in_stream 和 out_stream 说它们有 utf-8 编码,所以我认为编码没问题。现在,我只需将
stdin
设置为 UTF-8 TextIOWrapper
并使用 input()
(stdin.readline()
不起作用,它会冻结)即可修复冻结问题。但我不知道为什么它不能按原来的方式工作以及为什么这可以解决问题。如果有人可以解释并概述如何在将来避免此类问题(out_stream
也可能导致冻结吗?)我会接受他们的答案。
import sys
import io
sys.stdin = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8')
out_stream = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
def output(str):
out_stream.write(str)
def read_text():
try:
return input()
except EOFError:
return ""
text = read_text()
while text != "":
print("Outputting result", file=sys.stderr)
output(text)
print("Output done!", file=sys.stderr)
output("\n")
print("Flushing", file=sys.stderr)
out_stream.flush()
print("Flushed", file=sys.stderr)
text = read_text()
ProcessBuilder
的子进程有三个流,如果其中一个缓冲区被填满,Java和子进程之间的I/O会变得非常不高兴/冻结。
您写入和读取大量数据。对于所有
ProcessBuilder
调用,子进程可能会冻结,因为 Java 代码不读取 STDOUT,同时,如果子进程在 STDOUT 上阻塞而写入 STDIN,则 Java 代码似乎会冻结。反之亦然。所以永远不要在同一个线程中读写,也不要同时从 STDOUT+STDERR 读取。
最好的解决方法是对 STDIN/STDOUT/STDERR 使用单独的消费者/生产者线程。因为您已经使用了
INHERIT
模式来处理应该处理的 STDERR。
将
processText
分为 2 种方法:processText
执行 outputStream.write
且不执行 readLn
,以及 readText
执行 readLn
部分。在后台线程中运行 processText
并在主线程中使用 readText
。
调用
waitFor
后,请确保加入后台线程以确保它已完成。