Several puzzles in this chapter involved multiple threads, but
this one involves multiple processes. What does this program print if you run it
with the single command line argument slave? What does it print if you
run it with no command line arguments?
public class BeerBlast {
static final String COMMAND = "java BeerBlast slave";
public static void main(String[] args) throws Exception {
if (args.length == 1 && args[0].equals("slave")) {
for (int i = 99; i > 0; i--) {
System.out.println(i +
" bottles of beer on the wall");
System.out.println(i + " bottles of beer");
System.out.println(
"You take one down, pass it around,");
System.out.println((i-1) +
" bottles of beer on the wall");
System.out.println();
}
} else {
// Master
Process process = Runtime.getRuntime().exec(COMMAND);
int exitValue = process.waitFor();
System.out.println("exit value = " + exitValue);
}
}
}
Solution 82: Beer blast
If you run the program with the command line argument
slave, it prints a stirring rendition of that classic childhood ditty,
"99 Bottles of Beer on the Wall"—there's no mystery there. If you run it with no
command line argument, it starts a slave process that prints the ditty, but you
won't see the output of the slave process. The main process waits for the slave
process to finish and then prints the exit value of the slave. By convention,
the value 0 indicates normal termination, so that is what you might
expect the program to print. If you ran it, you probably found that it just hung
there, printing nothing at all. It's as if the slave process were taking
forever. Although it might feel like it takes
forever to listen to "99 Bottles of Beer on the Wall," especially if it is sung
out of tune, the song has "only" 99 verses. Besides, computers are fast, so
what's wrong with the program?
The clue to this mystery is in the documentation for the
Process class, which says: "Because some native platforms only provide
limited buffer size, failure to promptly read the output stream of the
subprocess may cause the subprocess to block, and even deadlock" [Java-API]. That is exactly
what's happening here: There is insufficient space in the buffer to hold the
interminable ditty. To ensure that the slave process terminates, the parent must
drain its output stream, which is an input stream from the perspective of the
master. The following utility method performs this task in a background
thread:
static void drainInBackground(final InputStream is) {
new Thread(new Runnable() {
public void run() {
try {
while(is.read() >= 0) ;
} catch (IOException e) {
// return on IOException
}
}
}).start();
}
If we modify the program to invoke this method prior to
waiting for the slave process, the program prints 0 as expected:
} else {
// Master
Process process = Runtime.getRuntime().exec(COMMAND);
drainInBackground(process.getInputStream());
int exitValue = process.waitFor();
System.out.println(exitValue);
}
The lesson is that you must drain the
output stream of a child process in order to ensure its termination; the same
goes for the error stream, which can be even more troublesome because you
can't predict when a process will dump lots of output to it. In release 5.0, a
class named ProcessBuilder was added to help you drain these streams.
Its redirectErrorStream method merges the streams so you have to drain
only one. If you elect not to merge the output and
error streams, you must drain them concurrently. Attempting to drain them
sequentially can cause the child process to hang.
Many programmers have been bitten by this bug over the years.
The lesson for API designers is that the Process class should have
prevented this problem, perhaps by draining the output and error streams
automatically unless the client expressed intent to read them. More generally,
APIs should make it easy to do the right thing and
difficult or impossible to do the wrong thing.
No comments:
Post a Comment
Your comments are welcome!