Wednesday, 19 September 2012

Puzzle 82: Beer Blast


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!